diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml new file mode 100644 index 0000000000000..4f0e9bb000a27 --- /dev/null +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml @@ -0,0 +1,35 @@ + + + + + + + + + <description value="AdminAnalytics Check Tracking."/> + <severity value="MINOR"/> + <testCaseId value="MC-36869"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <magentoCLI command="config:set admin/usage/enabled 1" stepKey="enableAdminUsageTracking"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> + <reloadPage stepKey="pageReload"/> + </before> + <after> + <magentoCLI command="config:set admin/usage/enabled 0" stepKey="disableAdminUsageTracking"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <waitForPageLoad stepKey="waitForPageReloaded"/> + <seeInPageSource html="var adminAnalyticsMetadata =" stepKey="seeInPageSource"/> + </test> +</tests> diff --git a/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml b/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml index e937a3e18148a..f16a66aa090e3 100644 --- a/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml +++ b/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml @@ -13,5 +13,24 @@ <value id="adobedtm" type="host">assets.adobedtm.com</value> </values> </policy> + <policy id="img-src"> + <values> + <value id="adobedtm" type="host">assets.adobedtm.com</value> + <value id="omtrdc" type="host">amcglobal.sc.omtrdc.net</value> + <value id="dpmdemdex" type="host">dpm.demdex.net</value> + <value id="everesttech" type="host">cm.everesttech.net</value> + </values> + </policy> + <policy id="connect-src"> + <values> + <value id="dpmdemdex" type="host">dpm.demdex.net</value> + <value id="omtrdc" type="host">amcglobal.sc.omtrdc.net</value> + </values> + </policy> + <policy id="frame-src"> + <values> + <value id="amcdemdex" type="host">fast.amc.demdex.net</value> + </values> + </policy> </policies> </csp_whitelist> diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminNotificationToolbarSection.xml b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminNotificationToolbarSection.xml new file mode 100644 index 0000000000000..c4a9290cb5641 --- /dev/null +++ b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminNotificationToolbarSection.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="AdminNotificationToolbarSection"> + <element name="notification" type="block" selector=".notifications-wrapper.admin__action-dropdown-wrap"/> + <element name="notificationCounter" type="block" selector=".notifications-action.admin__action-dropdown .notifications-counter"/> + </section> +</sections> diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.xml new file mode 100644 index 0000000000000..1ab277b4f788a --- /dev/null +++ b/app/code/Magento/AdminNotification/Test/Mftf/Test/AdminSystemNotificationToolbarBlockAclTest.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="AdminSystemNotificationToolbarBlockAclTest"> + <annotations> + <features value="AdminNotification"/> + <stories value="Acl notification toolbar"/> + <title value="Admin system notification toolbar block acl test"/> + <description value="Admin should not see system notification toolbar block if acl not restricted"/> + <severity value="MAJOR"/> + <testCaseId value="MC-36011"/> + <group value="menu"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Stores"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="goToRoleResourcesTab" /> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="addRestrictedRoleStores"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Products"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveUserRole" /> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsSaleRoleUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <!--Delete created data--> + <actionGroup ref="AdminUserOpenAdminRolesPageActionGroup" stepKey="navigateToUserRoleGrid"/> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <actionGroup ref="AdminOpenAdminUsersPageActionGroup" stepKey="goToAllUsersPage"/> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + </after> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + + <waitForPageLoad stepKey="waitBeforePageLoad"/> + <dontSeeElement selector="{{AdminNotificationToolbarSection.notification}}" stepKey="doNotSeeNotificationBellIcon"/> + </test> +</tests> diff --git a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml index eed6b53f34315..b71fbd40cadb7 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml +++ b/app/code/Magento/AdminNotification/view/adminhtml/layout/default.xml @@ -20,7 +20,11 @@ template="Magento_AdminNotification::notification/window.phtml"/> </referenceContainer> <referenceContainer name="header"> - <block class="Magento\AdminNotification\Block\ToolbarEntry" name="notification.messages" before="user" template="Magento_AdminNotification::toolbar_entry.phtml"/> + <block class="Magento\AdminNotification\Block\ToolbarEntry" + name="notification.messages" + before="user" + aclResource="Magento_AdminNotification::show_toolbar" + template="Magento_AdminNotification::toolbar_entry.phtml"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php index 974397226c56c..254dbcca852ee 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing.php @@ -10,7 +10,7 @@ use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; /** - * Class AdvancedPricing + * Import advanced pricing class * * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -19,35 +19,20 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity { const VALUE_ALL_GROUPS = 'ALL GROUPS'; - const VALUE_ALL_WEBSITES = 'All Websites'; - const COL_SKU = 'sku'; - const COL_TIER_PRICE_WEBSITE = 'tier_price_website'; - const COL_TIER_PRICE_CUSTOMER_GROUP = 'tier_price_customer_group'; - const COL_TIER_PRICE_QTY = 'tier_price_qty'; - const COL_TIER_PRICE = 'tier_price'; - const COL_TIER_PRICE_PERCENTAGE_VALUE = 'percentage_value'; - const COL_TIER_PRICE_TYPE = 'tier_price_value_type'; - const TIER_PRICE_TYPE_FIXED = 'Fixed'; - const TIER_PRICE_TYPE_PERCENT = 'Discount'; - const TABLE_TIER_PRICE = 'catalog_product_entity_tier_price'; - const DEFAULT_ALL_GROUPS_GROUPED_PRICE_VALUE = '0'; - const ENTITY_TYPE_CODE = 'advanced_pricing'; - const VALIDATOR_MAIN = 'validator'; - const VALIDATOR_WEBSITE = 'validator_website'; /** @@ -55,7 +40,6 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract * @see VALIDATOR_TIER_PRICE */ private const VALIDATOR_TEAR_PRICE = 'validator_tier_price'; - private const VALIDATOR_TIER_PRICE = 'validator_tier_price'; /** @@ -176,10 +160,8 @@ class AdvancedPricing extends \Magento\ImportExport\Model\Import\Entity\Abstract * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData * @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData - * @param \Magento\Eav\Model\Config $config * @param \Magento\Framework\App\ResourceConnection $resource * @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Framework\Stdlib\StringUtils $string * @param ProcessingErrorAggregatorInterface $errorAggregator * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime * @param \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory $resourceFactory @@ -197,10 +179,8 @@ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, \Magento\ImportExport\Helper\Data $importExportData, \Magento\ImportExport\Model\ResourceModel\Import\Data $importData, - \Magento\Eav\Model\Config $config, \Magento\Framework\App\ResourceConnection $resource, \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper, - \Magento\Framework\Stdlib\StringUtils $string, ProcessingErrorAggregatorInterface $errorAggregator, \Magento\Framework\Stdlib\DateTime\DateTime $dateTime, \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory $resourceFactory, 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 e57ed2c91409d..08d75f0f36f07 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php +++ b/app/code/Magento/AdvancedPricingImportExport/Test/Unit/Model/Import/AdvancedPricingTest.php @@ -16,7 +16,6 @@ use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as RowValidatorInterface; use Magento\CatalogImportExport\Model\Import\Product\StoreResolver; use Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory as ResourceFactory; -use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; @@ -26,7 +25,6 @@ use Magento\Framework\Json\Helper\Data; use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; -use Magento\Framework\Stdlib\StringUtils; use Magento\ImportExport\Model\Import; use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\ResourceModel\Helper; @@ -99,11 +97,6 @@ class AdvancedPricingTest extends AbstractImportTestCase */ protected $dataSourceModel; - /** - * @var Config - */ - protected $eavConfig; - /** * @var TimezoneInterface|MockObject */ @@ -139,11 +132,6 @@ class AdvancedPricingTest extends AbstractImportTestCase */ protected $advancedPricing; - /** - * @var StringUtils - */ - protected $stringObject; - /** * @var ProcessingErrorAggregatorInterface */ @@ -165,10 +153,8 @@ protected function setUp(): void ); $this->resource->method('getConnection')->willReturn($this->connection); $this->dataSourceModel = $this->createMock(\Magento\ImportExport\Model\ResourceModel\Import\Data::class); - $this->eavConfig = $this->createMock(Config::class); $entityType = $this->createMock(Type::class); $entityType->method('getEntityTypeId')->willReturn(''); - $this->eavConfig->method('getEntityType')->willReturn($entityType); $this->resourceFactory = $this->getMockBuilder( \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory::class ) @@ -193,7 +179,6 @@ protected function setUp(): void $this->tierPriceValidator = $this->createMock( TierPrice::class ); - $this->stringObject = $this->createMock(StringUtils::class); $this->errorAggregator = $this->getErrorAggregatorObject(); $this->dateTime = $this->getMockBuilder(DateTime::class) ->disableOriginalConstructor() @@ -1070,10 +1055,8 @@ private function getAdvancedPricingMock($methods = []) $this->jsonHelper, $this->importExportData, $this->dataSourceModel, - $this->eavConfig, $this->resource, $this->resourceHelper, - $this->stringObject, $this->errorAggregator, $this->dateTime, $this->resourceFactory, diff --git a/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AdminOpenConfigGeneralAnalyticsPageActionGroup.xml b/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AdminOpenConfigGeneralAnalyticsPageActionGroup.xml new file mode 100644 index 0000000000000..bfa3e436e09d0 --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AdminOpenConfigGeneralAnalyticsPageActionGroup.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="AdminOpenConfigGeneralAnalyticsPageActionGroup"> + <annotations> + <description>Open Config General Analytics Page.</description> + </annotations> + + <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <waitForPageLoad stepKey="waitPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml b/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml new file mode 100644 index 0000000000000..51d77228c8dcf --- /dev/null +++ b/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.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="AssertAdminAdvancedReportingPageUrlActionGroup"> + <annotations> + <description>Assert admin advanced reporting page url.</description> + </annotations> + + <switchToNextTab stepKey="switchToNewTab"/> + <waitForPageLoad stepKey="waitForAdvancedReportingPageLoad"/> + <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml index cbcbb3a5dd64c..9c99041be0df6 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingButtonTest.xml @@ -32,7 +32,6 @@ <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"/> + <actionGroup ref="AssertAdminAdvancedReportingPageUrlActionGroup" stepKey="assertAdvancedReportingPageUrl"/> </test> </tests> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml index ee25e80fcab30..f350452cfc7d0 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminAdvancedReportingNavigateMenuTest.xml @@ -29,8 +29,6 @@ <argument name="menuUiId" value="{{AdminMenuReports.dataUiId}}"/> <argument name="submenuUiId" value="{{AdminMenuReportsBusinessIntelligenceAdvancedReporting.dataUiId}}"/> </actionGroup> - <switchToNextTab stepKey="switchToNewTab"/> - <waitForPageLoad stepKey="waitForAdvancedReportingPageLoad"/> - <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> + <actionGroup ref="AssertAdminAdvancedReportingPageUrlActionGroup" stepKey="assertAdvancedReportingPageUrl"/> </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 17d463030d91c..a5b01a9221350 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationBlankIndustryTest.xml @@ -17,11 +17,13 @@ <testCaseId value="MAGETWO-63981"/> <group value="analytics"/> </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <actionGroup ref="AdminOpenConfigGeneralAnalyticsPageActionGroup" 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"/> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml index b03488c240604..6116dd72528f7 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationEnableDisableAnalyticsTest.xml @@ -17,11 +17,13 @@ <testCaseId value="MAGETWO-66465"/> <group value="analytics"/> </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <actionGroup ref="AdminOpenConfigGeneralAnalyticsPageActionGroup" 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"/> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml index c19fddc6aa0ce..1a77c365c8098 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationIndustryTest.xml @@ -18,9 +18,13 @@ <testCaseId value="MAGETWO-63898"/> <group value="analytics"/> </annotations> - - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + <actionGroup ref="AdminOpenConfigGeneralAnalyticsPageActionGroup" 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"/> diff --git a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml index 6231b17c17b02..60585e73baeaa 100644 --- a/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml +++ b/app/code/Magento/Analytics/Test/Mftf/Test/AdminConfigurationTimeToSendDataTest.xml @@ -18,11 +18,13 @@ <testCaseId value="MAGETWO-66464"/> <group value="analytics"/> </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <amOnPage url="{{AdminConfigGeneralAnalyticsPage.url}}" stepKey="amOnAdminConfig"/> + <actionGroup ref="AdminOpenConfigGeneralAnalyticsPageActionGroup" 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="23" stepKey="selectAdvancedReportingHour"/> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateToEmailToFriendSettingsActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateToEmailToFriendSettingsActionGroup.xml new file mode 100644 index 0000000000000..05903581747d9 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminNavigateToEmailToFriendSettingsActionGroup.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="AdminNavigateToEmailToFriendSettingsActionGroup"> + <amOnPage url="{{AdminConfigurationEmailToFriendPage.url}}" stepKey="navigateToPersistencePage"/> + <conditionalClick selector="{{AdminEmailToFriendSection.DefaultLayoutsTab}}" dependentSelector="{{AdminEmailToFriendSection.CheckIfTabExpand}}" visible="true" stepKey="clickTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminOpenGeneralConfigurationPageActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminOpenGeneralConfigurationPageActionGroup.xml new file mode 100644 index 0000000000000..ecacf063938ad --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminOpenGeneralConfigurationPageActionGroup.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="AdminOpenGeneralConfigurationPageActionGroup"> + <annotations> + <description>Open general configuration page.</description> + </annotations> + + <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="openGeneralConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminOpenWebConfigurationPageActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminOpenWebConfigurationPageActionGroup.xml new file mode 100644 index 0000000000000..e640eda7d653d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminOpenWebConfigurationPageActionGroup.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="AdminOpenWebConfigurationPageActionGroup"> + <annotations> + <description>Open web configuration page.</description> + </annotations> + + <amOnPage url="{{WebConfigurationPage.url}}" stepKey="openWebConfigurationPage"/> + <waitForPageLoad stepKey="waitPageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminEmailToFriendOptionsAvailableActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminEmailToFriendOptionsAvailableActionGroup.xml new file mode 100644 index 0000000000000..88152a2cb4f73 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminEmailToFriendOptionsAvailableActionGroup.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="AssertAdminEmailToFriendOptionsAvailableActionGroup"> + <seeElement stepKey="seeEmailTemplateInput" selector="{{AdminEmailToFriendSection.emailTemplate}}"/> + <seeElement stepKey="seeAllowForGuestsInput" selector="{{AdminEmailToFriendSection.allowForGuests}}"/> + <seeElement stepKey="seeMaxRecipientsInput" selector="{{AdminEmailToFriendSection.maxRecipients}}"/> + <seeElement stepKey="seeMaxPerHourInput" selector="{{AdminEmailToFriendSection.maxPerHour}}"/> + <seeElement stepKey="seeLimitSendingBy" selector="{{AdminEmailToFriendSection.limitSendingBy}}"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml new file mode 100644 index 0000000000000..09b0bdcc146ae --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.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="AssertAdminPageIs404ActionGroup"> + <annotations> + <description>Validates that the '404 Error' message is present in the current Admin Page Header.</description> + </annotations> + + <see userInput="404 Error" selector="{{AdminHeaderSection.pageHeading}}" stepKey="see404PageHeading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationEmailToFriendPage.xml b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationEmailToFriendPage.xml new file mode 100644 index 0000000000000..14bd514f1a16f --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Page/AdminConfigurationEmailToFriendPage.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="AdminConfigurationEmailToFriendPage" url="admin/system_config/edit/section/sendfriend/" module="Catalog" area="admin"> + <section name="AdminEmailToFriendSection"/> + </page> +</pages> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminCatalogEmailToFriendSettingsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminCatalogEmailToFriendSettingsTest.xml new file mode 100644 index 0000000000000..b410a4cb73de7 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminCatalogEmailToFriendSettingsTest.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="AdminCatalogEmailToFriendSettingsTest"> + <annotations> + <features value="Backend"/> + <stories value="Enable Email To A Friend Functionality"/> + <title value="Admin should be able to manage settings of Email To A Friend Functionality"/> + <description value="Admin should be able to enable Email To A Friend functionality in Magento Admin backend and see additional options"/> + <group value="backend"/> + <severity value="MINOR"></severity> + <testCaseId value="MC-35895"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <magentoCLI stepKey="enableSendFriend" command="config:set sendfriend/email/enabled 1"/> + <magentoCLI stepKey="cacheClean" command="cache:clean config"/> + </before> + <after> + <magentoCLI stepKey="disableSendFriend" command="config:set sendfriend/email/enabled 0"/> + <magentoCLI stepKey="cacheClean" command="cache:clean config"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminNavigateToEmailToFriendSettingsActionGroup" stepKey="navigateToSendFriendSettings"/> + <actionGroup ref="AssertAdminEmailToFriendOptionsAvailableActionGroup" stepKey="assertOptions"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Model/Product/BundleOptionDataProvider.php b/app/code/Magento/Bundle/Model/Product/BundleOptionDataProvider.php new file mode 100644 index 0000000000000..f56c4228e49e5 --- /dev/null +++ b/app/code/Magento/Bundle/Model/Product/BundleOptionDataProvider.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Bundle\Model\Product; + +use Magento\Bundle\Helper\Catalog\Product\Configuration; +use Magento\Bundle\Model\Option; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\Framework\Pricing\Helper\Data; +use Magento\Framework\Serialize\SerializerInterface; + +/** + * Data provider for bundled product options + */ +class BundleOptionDataProvider +{ + /** + * @var Data + */ + private $pricingHelper; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var Configuration + */ + private $configuration; + + /** + * @param Data $pricingHelper + * @param SerializerInterface $serializer + * @param Configuration $configuration + */ + public function __construct( + Data $pricingHelper, + SerializerInterface $serializer, + Configuration $configuration + ) { + $this->pricingHelper = $pricingHelper; + $this->serializer = $serializer; + $this->configuration = $configuration; + } + + /** + * Extract data for a bundled item + * + * @param ItemInterface $item + * + * @return array + */ + public function getData(ItemInterface $item): array + { + $options = []; + $product = $item->getProduct(); + $optionsQuoteItemOption = $item->getOptionByCode('bundle_option_ids'); + $bundleOptionsIds = $optionsQuoteItemOption + ? $this->serializer->unserialize($optionsQuoteItemOption->getValue()) + : []; + + /** @var Type $typeInstance */ + $typeInstance = $product->getTypeInstance(); + + if ($bundleOptionsIds) { + $selectionsQuoteItemOption = $item->getOptionByCode('bundle_selection_ids'); + $optionsCollection = $typeInstance->getOptionsByIds($bundleOptionsIds, $product); + $bundleSelectionIds = $this->serializer->unserialize($selectionsQuoteItemOption->getValue()); + + if (!empty($bundleSelectionIds)) { + $selectionsCollection = $typeInstance->getSelectionsByIds($bundleSelectionIds, $product); + $bundleOptions = $optionsCollection->appendSelections($selectionsCollection, true); + + $options = $this->buildBundleOptions($bundleOptions, $item); + } + } + + return $options; + } + + /** + * Build bundle product options based on current selection + * + * @param Option[] $bundleOptions + * @param ItemInterface $item + * + * @return array + */ + private function buildBundleOptions(array $bundleOptions, ItemInterface $item): array + { + $options = []; + foreach ($bundleOptions as $bundleOption) { + if (!$bundleOption->getSelections()) { + continue; + } + + $options[] = [ + 'id' => $bundleOption->getId(), + 'label' => $bundleOption->getTitle(), + 'type' => $bundleOption->getType(), + 'values' => $this->buildBundleOptionValues($bundleOption->getSelections(), $item), + ]; + } + + return $options; + } + + /** + * Build bundle product option values based on current selection + * + * @param Product[] $selections + * @param ItemInterface $item + * + * @return array + */ + private function buildBundleOptionValues(array $selections, ItemInterface $item): array + { + $product = $item->getProduct(); + $values = []; + + foreach ($selections as $selection) { + $qty = (float) $this->configuration->getSelectionQty($product, $selection->getSelectionId()); + if (!$qty) { + continue; + } + + $selectionPrice = $this->configuration->getSelectionFinalPrice($item, $selection); + $values[] = [ + 'label' => $selection->getName(), + 'id' => $selection->getSelectionId(), + 'quantity' => $qty, + 'price' => $this->pricingHelper->currency($selectionPrice, false, false), + ]; + } + + return $values; + } +} diff --git a/app/code/Magento/Bundle/Model/Product/LinksList.php b/app/code/Magento/Bundle/Model/Product/LinksList.php index aeb71d0e434ab..c35d475e04d84 100644 --- a/app/code/Magento/Bundle/Model/Product/LinksList.php +++ b/app/code/Magento/Bundle/Model/Product/LinksList.php @@ -39,6 +39,8 @@ public function __construct( } /** + * Bundle Product Items Data + * * @param \Magento\Catalog\Api\Data\ProductInterface $product * @param int $optionId * @return \Magento\Bundle\Api\Data\LinkInterface[] @@ -50,8 +52,12 @@ public function getItems(\Magento\Catalog\Api\Data\ProductInterface $product, $o $productLinks = []; /** @var \Magento\Catalog\Model\Product $selection */ foreach ($selectionCollection as $selection) { + $bundledProductPrice = $selection->getSelectionPriceValue(); + if ($bundledProductPrice <= 0) { + $bundledProductPrice = $selection->getPrice(); + } $selectionPriceType = $product->getPriceType() ? $selection->getSelectionPriceType() : null; - $selectionPrice = $product->getPriceType() ? $selection->getSelectionPriceValue() : null; + $selectionPrice = $bundledProductPrice ? $bundledProductPrice : null; /** @var \Magento\Bundle\Api\Data\LinkInterface $productLink */ $productLink = $this->linkFactory->create(); diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php index 7b3f6dd8bbefa..303c33b571d35 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Selection/Collection.php @@ -215,7 +215,7 @@ public function joinPrices($websiteId) public function setOptionIdsFilter($optionIds) { if (!empty($optionIds)) { - $this->getSelect()->where('selection.option_id IN (?)', $optionIds); + $this->getSelect()->where('selection.option_id IN (?)', $optionIds, \Zend_Db::INT_TYPE); } return $this; } @@ -229,7 +229,7 @@ public function setOptionIdsFilter($optionIds) public function setSelectionIdsFilter($selectionIds) { if (!empty($selectionIds)) { - $this->getSelect()->where('selection.selection_id IN (?)', $selectionIds); + $this->getSelect()->where('selection.selection_id IN (?)', $selectionIds, \Zend_Db::INT_TYPE); } return $this; } diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.xml new file mode 100644 index 0000000000000..0e2ae9bf5cc5f --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest.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="StorefrontBundlePlaceOrderWithMultipleOptionsSuccessTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product details page"/> + <title value="Customer should be able to see all the bundle items in invoice view"/> + <description value="Customer should be able to see all the bundle items in invoice view"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37515"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct2" stepKey="firstSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="secondSimpleProduct"/> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + </before> + <after> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="firstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="secondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Create new bundle product --> + <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createBundleProduct"> + <argument name="productType" value="bundle"/> + </actionGroup> + + <!-- Fill all main fields --> + <actionGroup ref="FillMainBundleProductFormActionGroup" stepKey="fillMainProductFields"/> + + <!-- Add first bundle option to the product --> + <actionGroup ref="AddBundleOptionWithTwoProductsActionGroup" stepKey="addFirstBundleOption"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$firstSimpleProduct.sku$"/> + <argument name="prodTwoSku" value="$secondSimpleProduct.sku$$"/> + <argument name="optionTitle" value="{{CheckboxOption.title}}"/> + <argument name="inputType" value="{{CheckboxOption.type}}"/> + </actionGroup> + + <!-- Save product form --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveWithThreeOptions"/> + + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + + <!--Open Product Page--> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> + <argument name="productUrl" value="{{BundleProduct.name}}"/> + </actionGroup> + + <!-- Add bundle to cart --> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"> + <argument name="productUrl" value="{{BundleProduct.name}}"/> + </actionGroup> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts(CheckboxOption.title, '1')}}" stepKey="selectOption2Product1"/> + <checkOption selector="{{StorefrontBundledSection.checkboxOptionThreeProducts(CheckboxOption.title, '2')}}" stepKey="selectOption2Product2"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + + <!--Navigate to checkout--> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/> + <!-- Click next button to open payment section --> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!-- Click place order --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Order review page has address that was created during checkout --> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrdersGridById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + + <!-- Open create invoice page --> + <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> + + <!-- Assert item options display --> + <see selector="{{AdminInvoiceItemsSection.bundleItem}}" userInput="50 x $firstSimpleProduct.sku$" stepKey="seeFirstProductInList"/> + <see selector="{{AdminInvoiceItemsSection.bundleItem}}" userInput="50 x $secondSimpleProduct.sku$" stepKey="seeSecondProductInList"/> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml index f6866f813f258..88fc5b7171592 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml @@ -71,8 +71,7 @@ <!--Click add to cart--> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="addProductToCart"/> <!--Check for details page--> <seeInCurrentUrl url="{{BundleProduct.sku}}" stepKey="seeBundleProductDetailsPage"/> diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php index fbc3b5e87ac97..27531682b1de2 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/LinksListTest.php @@ -91,7 +91,7 @@ public function testLinksList() ->method('getSelectionsCollection') ->with([$optionId], $this->productMock) ->willReturn([$this->selectionMock]); - $this->productMock->expects($this->exactly(2))->method('getPriceType')->willReturn('price_type'); + $this->productMock->expects($this->once())->method('getPriceType')->willReturn('price_type'); $this->selectionMock->expects($this->once()) ->method('getSelectionPriceType') ->willReturn('selection_price_type'); 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 23e7ef27fa78d..15ef3c311e396 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 @@ -15,6 +15,7 @@ <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php $canEditItemQty = true ?> <?php /** @var \Magento\Catalog\Helper\Data $catalogHelper */ $catalogHelper = $block->getData('catalogHelper'); @@ -37,7 +38,7 @@ $catalogHelper = $block->getData('catalogHelper'); <?php if ($_item->getOrderItem()->getParentItem()): ?> <?php if ($shipTogether) { - continue; + $canEditItemQty = false; } ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> @@ -130,7 +131,7 @@ $catalogHelper = $block->getData('catalogHelper'); </td> <td class="col-qty-invoice"> <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> - <?php if ($block->canEditQty()): ?> + <?php if ($block->canEditQty() && $canEditItemQty): ?> <input type="text" class="input-text admin__control-text qty-input" name="invoice[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php index 13bf10bc6aca7..8025cf91d28c9 100644 --- a/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Links/Collection.php @@ -108,7 +108,7 @@ private function fetch() : array } $linkCollection->getSelect() - ->where($field . ' IN (?)', $this->parentIds); + ->where($field . ' IN (?)', $this->parentIds, \Zend_Db::INT_TYPE); /** @var Selection $link */ foreach ($linkCollection as $link) { diff --git a/app/code/Magento/BundleGraphQl/Model/Wishlist/BundleOptions.php b/app/code/Magento/BundleGraphQl/Model/Wishlist/BundleOptions.php new file mode 100644 index 0000000000000..217f822e771da --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Wishlist/BundleOptions.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Wishlist; + +use Magento\Bundle\Model\Product\BundleOptionDataProvider; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +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; + +/** + * Fetches the selected bundle options + */ +class BundleOptions implements ResolverInterface +{ + /** + * @var BundleOptionDataProvider + */ + private $bundleOptionDataProvider; + + /** + * @param BundleOptionDataProvider $bundleOptionDataProvider + */ + public function __construct( + BundleOptionDataProvider $bundleOptionDataProvider + ) { + $this->bundleOptionDataProvider = $bundleOptionDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$value['itemModel'] instanceof ItemInterface) { + throw new LocalizedException(__('"itemModel" should be a "%instance" instance', [ + 'instance' => ItemInterface::class + ])); + } + + return $this->bundleOptionDataProvider->getData($value['itemModel']); + } +} diff --git a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml index 863e152fbe177..7fe0b2a53677c 100644 --- a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml @@ -100,4 +100,11 @@ </argument> </arguments> </type> + <type name="Magento\WishlistGraphQl\Model\Resolver\Type\WishlistItemType"> + <arguments> + <argument name="supportedTypes" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleWishlistItem</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls index a66fa397020a7..a2cba24c7c4d4 100644 --- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls @@ -117,3 +117,7 @@ type ItemSelectedBundleOptionValue @doc(description: "A list of values for the s quantity: Float! @doc(description: "Indicates how many of this bundle product were ordered") price: Money! @doc(description: "The price of the child bundle product") } + +type BundleWishlistItem implements WishlistItemInterface { + bundle_options: [SelectedBundleOption!] @doc(description: "An array containing information about the selected bundle items") @resolver(class: "\\Magento\\BundleGraphQl\\Model\\Wishlist\\BundleOptions") +} diff --git a/app/code/Magento/Captcha/CustomerData/Captcha.php b/app/code/Magento/Captcha/CustomerData/Captcha.php index e07bf953abaa3..901477c75610b 100644 --- a/app/code/Magento/Captcha/CustomerData/Captcha.php +++ b/app/code/Magento/Captcha/CustomerData/Captcha.php @@ -58,7 +58,7 @@ public function __construct( /** * @inheritdoc */ - public function getSectionData() :array + public function getSectionData(): array { $data = []; diff --git a/app/code/Magento/CardinalCommerce/Test/Mftf/ActionGroup/AdminOpenAdminThreeDSecurePageActionGroup.xml b/app/code/Magento/CardinalCommerce/Test/Mftf/ActionGroup/AdminOpenAdminThreeDSecurePageActionGroup.xml new file mode 100644 index 0000000000000..baea26921a625 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/Test/Mftf/ActionGroup/AdminOpenAdminThreeDSecurePageActionGroup.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="AdminOpenAdminThreeDSecurePageActionGroup"> + <annotations> + <description>Open ThreeDSecure page.</description> + </annotations> + + <amOnPage url="{{AdminThreeDSecurePage.url}}" stepKey="openAdminThreeDSecurePage"/> + <waitForPageLoad stepKey="waitThreeDSecurePageToLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php index a25501d9ef150..6cc5652352154 100644 --- a/app/code/Magento/Catalog/Block/Product/View.php +++ b/app/code/Magento/Catalog/Block/Product/View.php @@ -196,6 +196,10 @@ public function getJsonConfig() 'productId' => (int)$product->getId(), 'priceFormat' => $this->_localeFormat->getPriceFormat(), 'prices' => [ + 'baseOldPrice' => [ + 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() * 1, + 'adjustments' => [] + ], 'oldPrice' => [ 'amount' => $priceInfo->getPrice('regular_price')->getAmount()->getValue() * 1, 'adjustments' => [] diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index a06266037d05c..ab74b5694ce9f 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -384,7 +384,9 @@ public function backgroundColor($colorRGB) { // assume that 3 params were given instead of array if (!is_array($colorRGB)) { + //phpcs:disable $colorRGB = func_get_args(); + //phpcs:enabled } $this->_getModel()->setBackgroundColor($colorRGB); return $this; @@ -498,7 +500,11 @@ protected function initBaseFile() if ($this->getImageFile()) { $model->setBaseFile($this->getImageFile()); } else { - $model->setBaseFile($this->getProduct()->getData($model->getDestinationSubdir())); + $model->setBaseFile( + $this->getProduct() + ? $this->getProduct()->getData($model->getDestinationSubdir()) + : '' + ); } } return $this; 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 1506ccf6963bf..ae24b60719ca7 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/AbstractAction.php @@ -6,10 +6,21 @@ namespace Magento\Catalog\Model\Indexer\Category\Flat; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Helper; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\EntityManager\EntityMetadata; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; /** * Abstract action class for category flat indexers. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class AbstractAction { @@ -31,14 +42,14 @@ class AbstractAction protected $resource; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** * Catalog resource helper * - * @var \Magento\Catalog\Model\ResourceModel\Helper + * @var Helper */ protected $resourceHelper; @@ -50,12 +61,12 @@ class AbstractAction protected $columns = []; /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface + * @var AdapterInterface */ protected $connection; /** - * @var \Magento\Framework\EntityManager\EntityMetadata + * @var EntityMetadata */ protected $categoryMetadata; @@ -68,13 +79,13 @@ class AbstractAction /** * @param ResourceConnection $resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper + * @param StoreManagerInterface $storeManager + * @param Helper $resourceHelper */ public function __construct( ResourceConnection $resource, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper + StoreManagerInterface $storeManager, + Helper $resourceHelper ) { $this->resource = $resource; $this->connection = $resource->getConnection(); @@ -110,23 +121,22 @@ public function getColumns() * @param integer $storeId * @return string */ - public function getMainStoreTable($storeId = \Magento\Store\Model\Store::DEFAULT_STORE_ID) + public function getMainStoreTable($storeId = Store::DEFAULT_STORE_ID) { if (is_string($storeId)) { $storeId = (int) $storeId; } $suffix = sprintf('store_%d', $storeId); - $table = $this->connection->getTableName($this->getTableName('catalog_category_flat_' . $suffix)); - - return $table; + return $this->connection->getTableName($this->getTableName('catalog_category_flat_' . $suffix)); } /** * Return structure for flat catalog table * * @param string $tableName - * @return \Magento\Framework\DB\Ddl\Table + * @return Table + * @throws \Zend_Db_Exception */ protected function getFlatTableStructure($tableName) { @@ -139,10 +149,10 @@ protected function getFlatTableStructure($tableName) //Adding columns foreach ($this->getColumns() as $fieldName => $fieldProp) { $default = $fieldProp['default']; - if ($fieldProp['type'][0] == \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP + if ($fieldProp['type'][0] == Table::TYPE_TIMESTAMP && $default == 'CURRENT_TIMESTAMP' ) { - $default = \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT; + $default = Table::TIMESTAMP_INIT; } $table->addColumn( $fieldName, @@ -205,9 +215,9 @@ protected function getStaticColumns() $ddlType = $this->resourceHelper->getDdlTypeByColumnType($column['DATA_TYPE']); $column['DEFAULT'] = trim($column['DEFAULT'], "' "); switch ($ddlType) { - case \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT: - case \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER: - case \Magento\Framework\DB\Ddl\Table::TYPE_BIGINT: + case Table::TYPE_SMALLINT: + case Table::TYPE_INTEGER: + case Table::TYPE_BIGINT: $isUnsigned = (bool)$column['UNSIGNED']; if ($column['DEFAULT'] === '') { $column['DEFAULT'] = null; @@ -215,27 +225,27 @@ protected function getStaticColumns() $options = null; if ($column['SCALE'] > 0) { - $ddlType = \Magento\Framework\DB\Ddl\Table::TYPE_DECIMAL; + $ddlType = Table::TYPE_DECIMAL; } else { break; } // fall-through intentional - case \Magento\Framework\DB\Ddl\Table::TYPE_DECIMAL: + case Table::TYPE_DECIMAL: $options = $column['PRECISION'] . ',' . $column['SCALE']; $isUnsigned = null; if ($column['DEFAULT'] === '') { $column['DEFAULT'] = null; } break; - case \Magento\Framework\DB\Ddl\Table::TYPE_TEXT: + case Table::TYPE_TEXT: $options = $column['LENGTH']; $isUnsigned = null; break; - case \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP: + case Table::TYPE_TIMESTAMP: $options = null; $isUnsigned = null; break; - case \Magento\Framework\DB\Ddl\Table::TYPE_DATETIME: + case Table::TYPE_DATETIME: $isUnsigned = null; break; } @@ -248,7 +258,7 @@ protected function getStaticColumns() ]; } $columns['store_id'] = [ - 'type' => [\Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, 5], + 'type' => [Table::TYPE_SMALLINT, 5], 'unsigned' => true, 'nullable' => false, 'default' => '0', @@ -274,7 +284,7 @@ protected function getEavColumns() switch ($attribute['backend_type']) { case 'varchar': $columns[$attribute['attribute_code']] = [ - 'type' => [\Magento\Framework\DB\Ddl\Table::TYPE_TEXT, 255], + 'type' => [Table::TYPE_TEXT, 255], 'unsigned' => null, 'nullable' => true, 'default' => null, @@ -283,7 +293,7 @@ protected function getEavColumns() break; case 'int': $columns[$attribute['attribute_code']] = [ - 'type' => [\Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, null], + 'type' => [Table::TYPE_INTEGER, null], 'unsigned' => null, 'nullable' => true, 'default' => null, @@ -292,7 +302,7 @@ protected function getEavColumns() break; case 'text': $columns[$attribute['attribute_code']] = [ - 'type' => [\Magento\Framework\DB\Ddl\Table::TYPE_TEXT, '64k'], + 'type' => [Table::TYPE_TEXT, '64k'], 'unsigned' => null, 'nullable' => true, 'default' => null, @@ -301,7 +311,7 @@ protected function getEavColumns() break; case 'datetime': $columns[$attribute['attribute_code']] = [ - 'type' => [\Magento\Framework\DB\Ddl\Table::TYPE_DATETIME, null], + 'type' => [Table::TYPE_DATETIME, null], 'unsigned' => null, 'nullable' => true, 'default' => null, @@ -310,7 +320,7 @@ protected function getEavColumns() break; case 'decimal': $columns[$attribute['attribute_code']] = [ - 'type' => [\Magento\Framework\DB\Ddl\Table::TYPE_DECIMAL, '12,4'], + 'type' => [Table::TYPE_DECIMAL, '12,4'], 'unsigned' => null, 'nullable' => true, 'default' => null, @@ -346,7 +356,7 @@ protected function getAttributes() $this->connection->getTableName( $this->getTableName('eav_entity_type') ) . '.entity_type_code = ?', - \Magento\Catalog\Model\Category::ENTITY + Category::ENTITY ); $this->attributeCodes = []; foreach ($this->connection->fetchAll($select) as $attribute) { @@ -414,7 +424,8 @@ private function getLinkIds(array $entityIds) [$linkField] )->where( 'e.entity_id IN (?)', - $entityIds + $entityIds, + \Zend_Db::INT_TYPE ); return $this->connection->fetchCol($select); @@ -459,10 +470,12 @@ protected function getAttributeTypeValues($type, $entityIds, $storeId) ] )->where( "e.entity_id IN (?)", - $entityIds + $entityIds, + \Zend_Db::INT_TYPE )->where( 'def.store_id IN (?)', - [\Magento\Store\Model\Store::DEFAULT_STORE_ID, $storeId] + [Store::DEFAULT_STORE_ID, $storeId], + \Zend_Db::INT_TYPE ); return $this->connection->fetchAll($select); @@ -501,14 +514,14 @@ protected function getTableName($name) /** * Get category metadata instance. * - * @return \Magento\Framework\EntityManager\EntityMetadata + * @return EntityMetadata */ private function getCategoryMetadata() { if (null === $this->categoryMetadata) { - $metadataPool = \Magento\Framework\App\ObjectManager::getInstance() + $metadataPool = ObjectManager::getInstance() ->get(\Magento\Framework\EntityManager\MetadataPool::class); - $this->categoryMetadata = $metadataPool->getMetadata(\Magento\Catalog\Api\Data\CategoryInterface::class); + $this->categoryMetadata = $metadataPool->getMetadata(CategoryInterface::class); } return $this->categoryMetadata; } @@ -521,8 +534,8 @@ private function getCategoryMetadata() private function getSkipStaticColumns() { if (null === $this->skipStaticColumns) { - $provider = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\Indexer\Category\Flat\SkipStaticColumnsProvider::class); + $provider = ObjectManager::getInstance() + ->get(SkipStaticColumnsProvider::class); $this->skipStaticColumns = $provider->get(); } return $this->skipStaticColumns; diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php index c722206193eb3..20f01e4b0a0ab 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Flat/Action/Rows.php @@ -119,7 +119,8 @@ protected function filterIdsByStore(array $ids, $store) "path = {$rootIdExpr} OR path = {$rootCatIdExpr} OR path like {$catIdExpr}" )->where( "entity_id IN (?)", - $ids + $ids, + \Zend_Db::INT_TYPE ); $resultIds = []; @@ -170,27 +171,30 @@ private function buildIndexData(Store $store, $categoriesIdsChunk, $attributesDa foreach ($categoriesIdsChunk as $categoryId) { try { $category = $this->categoryRepository->get($categoryId); - $categoryData = $category->getData(); - $linkId = $categoryData[$linkField]; - - $categoryAttributesData = []; - if (isset($attributesData[$linkId]) && is_array($attributesData[$linkId])) { - $categoryAttributesData = $attributesData[$linkId]; - } - $categoryIndexData = $this->buildCategoryIndexData( - $store, - $categoryData, - $categoryAttributesData - ); - $data[] = $categoryIndexData; } catch (NoSuchEntityException $e) { - // ignore + continue; } + + $categoryData = $category->getData(); + $linkId = $categoryData[$linkField]; + + $categoryAttributesData = []; + if (isset($attributesData[$linkId]) && is_array($attributesData[$linkId])) { + $categoryAttributesData = $attributesData[$linkId]; + } + $categoryIndexData = $this->buildCategoryIndexData( + $store, + $categoryData, + $categoryAttributesData + ); + $data[] = $categoryIndexData; } return $data; } /** + * Prepare Category data + * * @param Store $store * @param array $categoryData * @param array $categoryAttributesData @@ -213,7 +217,8 @@ private function buildCategoryIndexData(Store $store, array $categoryData, array * Insert or update index data * * @param string $tableName - * @param $data + * @param array $data + * @return void */ private function updateIndexData($tableName, $data) { 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 6b6ad2bfc726a..38f606b8abefe 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -15,6 +15,7 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\Store; +// phpcs:disable Magento2.Classes.AbstractApi /** * Class AbstractAction * @@ -42,7 +43,7 @@ abstract class AbstractAction /** * Suffix for table to show it is temporary - * @deprecated + * @deprecated see getIndexTable */ const TEMPORARY_TABLE_SUFFIX = '_tmp'; @@ -504,10 +505,11 @@ protected function createAnchorSelect(Store $store) [] )->joinInner( ['cc2' => $temporaryTreeTable], - 'cc2.parent_id = cc.entity_id AND cc.entity_id NOT IN (' . implode( - ',', - $rootCatIds - ) . ')', + $this->connection->quoteInto( + 'cc2.parent_id = cc.entity_id AND cc.entity_id NOT IN (?)', + $rootCatIds, + \Zend_Db::INT_TYPE + ), [] )->joinInner( ['ccp' => $this->getTable('catalog_category_product')], 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 eee347c36910d..a7c5cdf412e6e 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 @@ -174,7 +174,7 @@ protected function reindex(): void foreach ($this->storeManager->getStores() as $store) { if ($this->getPathFromCategoryId($store->getRootCategoryId())) { $userFunctions[$store->getId()] = function () use ($store) { - return $this->reindexStore($store); + $this->reindexStore($store); }; } } @@ -282,7 +282,7 @@ private function reindexCategoriesBySelect(Select $basicSelect, $whereCondition, $this->connection->delete($this->tableMaintainer->getMainTmpTable((int)$store->getId())); $entityIds = $this->connection->fetchCol($query); $resultSelect = clone $basicSelect; - $resultSelect->where($whereCondition, $entityIds); + $resultSelect->where($whereCondition, $entityIds, \Zend_Db::INT_TYPE); $this->connection->query( $this->connection->insertFromSelect( $resultSelect, 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 ec3d0d57330ec..edd68422ec4ac 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 @@ -151,7 +151,7 @@ private function getProductIdsWithParents(array $childProductIds): array ->select() ->from(['relation' => $this->getTable('catalog_product_relation')], []) ->distinct(true) - ->where('child_id IN (?)', $childProductIds) + ->where('child_id IN (?)', $childProductIds, \Zend_Db::INT_TYPE) ->join( ['cpe' => $this->getTable('catalog_product_entity')], 'relation.parent_id = cpe.' . $fieldForParent, @@ -215,7 +215,7 @@ protected function removeEntries() protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $store) { $select = parent::getNonAnchorCategoriesSelect($store); - return $select->where('ccp.product_id IN (?)', $this->limitationByProducts); + return $select->where('ccp.product_id IN (?)', $this->limitationByProducts, \Zend_Db::INT_TYPE); } /** @@ -227,7 +227,7 @@ protected function getNonAnchorCategoriesSelect(\Magento\Store\Model\Store $stor protected function getAnchorCategoriesSelect(\Magento\Store\Model\Store $store) { $select = parent::getAnchorCategoriesSelect($store); - return $select->where('ccp.product_id IN (?)', $this->limitationByProducts); + return $select->where('ccp.product_id IN (?)', $this->limitationByProducts, \Zend_Db::INT_TYPE); } /** @@ -239,7 +239,7 @@ protected function getAnchorCategoriesSelect(\Magento\Store\Model\Store $store) protected function getAllProducts(\Magento\Store\Model\Store $store) { $select = parent::getAllProducts($store); - return $select->where('cp.entity_id IN (?)', $this->limitationByProducts); + return $select->where('cp.entity_id IN (?)', $this->limitationByProducts, \Zend_Db::INT_TYPE); } /** @@ -265,7 +265,7 @@ private function getCategoryIdsFromIndex(array $productIds): array $storeCategories = $this->connection->fetchCol( $this->connection->select() ->from($this->getIndexTable($store->getId()), ['category_id']) - ->where('product_id IN (?)', $productIds) + ->where('product_id IN (?)', $productIds, \Zend_Db::INT_TYPE) ->distinct() ); $categoryIds[] = $storeCategories; 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 2252b3e3d5506..99d75186eca8c 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php @@ -6,8 +6,19 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Helper\Product\Flat\Indexer; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Ddl\Table; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Framework\App\ObjectManager; +use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\DB\Select; /** * Class for building flat index @@ -27,22 +38,22 @@ class FlatTableBuilder const XML_NODE_MAX_INDEX_COUNT = 'catalog/product/flat/max_index_count'; /** - * @var \Magento\Catalog\Helper\Product\Flat\Indexer + * @var Indexer */ protected $_productIndexerHelper; /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface + * @var AdapterInterface */ protected $_connection; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface $config + * @var ScopeConfigInterface $config */ protected $_config; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; @@ -52,23 +63,23 @@ class FlatTableBuilder protected $_tableData; /** - * @var \Magento\Framework\App\ResourceConnection + * @var ResourceConnection */ protected $resource; /** - * @param \Magento\Catalog\Helper\Product\Flat\Indexer $productIndexerHelper + * @param Indexer $productIndexerHelper * @param ResourceConnection $resource - * @param \Magento\Framework\App\Config\ScopeConfigInterface $config - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @param ScopeConfigInterface $config + * @param StoreManagerInterface $storeManager * @param TableDataInterface $tableData */ public function __construct( - \Magento\Catalog\Helper\Product\Flat\Indexer $productIndexerHelper, - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Framework\App\Config\ScopeConfigInterface $config, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Catalog\Model\Indexer\Product\Flat\TableDataInterface $tableData + Indexer $productIndexerHelper, + ResourceConnection $resource, + ScopeConfigInterface $config, + StoreManagerInterface $storeManager, + TableDataInterface $tableData ) { $this->_productIndexerHelper = $productIndexerHelper; $this->resource = $resource; @@ -114,7 +125,7 @@ public function build($storeId, $changedIds, $valueFieldSuffix, $tableDropSuffix * * @param int|string $storeId * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -128,7 +139,7 @@ protected function _createTemporaryFlatTable($storeId) self::XML_NODE_MAX_INDEX_COUNT ); if ($maxIndex && count($indexesNeed) > $maxIndex) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __( 'The Flat Catalog module has a limit of %2$d filterable and/or sortable attributes.' . 'Currently there are %1$d of them.' @@ -141,7 +152,7 @@ protected function _createTemporaryFlatTable($storeId) $indexKeys = []; $indexProps = array_values($indexesNeed); - $upperPrimaryKey = strtoupper(\Magento\Framework\DB\Adapter\AdapterInterface::INDEX_TYPE_PRIMARY); + $upperPrimaryKey = strtoupper(AdapterInterface::INDEX_TYPE_PRIMARY); foreach ($indexProps as $i => $indexProp) { $indexName = $this->_connection->getIndexName( $this->_getTemporaryTableName($this->_productIndexerHelper->getFlatTableName($storeId)), @@ -164,7 +175,7 @@ protected function _createTemporaryFlatTable($storeId) } $indexesNeed = array_combine($indexKeys, $indexProps); - /** @var $table \Magento\Framework\DB\Ddl\Table */ + /** @var $table Table */ $table = $this->_connection->newTable( $this->_getTemporaryTableName($this->_productIndexerHelper->getFlatTableName($storeId)) ); @@ -211,6 +222,8 @@ protected function _createTemporaryFlatTable($storeId) * @param int|string $storeId * @param string $valueFieldSuffix * @return void + * @throws LocalizedException + * @throws NoSuchEntityException */ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldSuffix) { @@ -226,14 +239,14 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS $websiteId = (int)$this->_storeManager->getStore($storeId)->getWebsiteId(); unset($tables[$entityTableName]); - - $allColumns = array_values( + $allColumns = []; + $allColumns[] = array_values( array_unique( array_merge(['entity_id', $linkField, 'type_id', 'attribute_set_id'], $columnsList) ) ); - /* @var $status \Magento\Eav\Model\Entity\Attribute */ + /* @var $status Attribute */ $status = $this->_productIndexerHelper->getAttribute('status'); $statusTable = $this->_getTemporaryTableName($status->getBackendTable()); $statusConditions = [ @@ -248,7 +261,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS $select->from( ['et' => $entityTemporaryTableName], - $allColumns + array_merge(...$allColumns) )->joinInner( ['e' => $this->resource->getTableName('catalog_product_entity')], 'e.entity_id = et.entity_id', @@ -262,7 +275,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS implode(' AND ', $statusConditions), [] )->where( - $statusExpression . ' = ' . \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED + $statusExpression . ' = ' . Status::STATUS_ENABLED ); foreach ($tables as $tableName => $columns) { @@ -276,7 +289,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS sprintf('e.%1$s = %2$s.%1$s', $linkField, $temporaryTableName), $columnsNames ); - $allColumns = array_merge($allColumns, $columnsNames); + $allColumns[] = $columnsNames; foreach ($columnsNames as $name) { $columnValueName = $name . $valueFieldSuffix; @@ -290,10 +303,10 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS sprintf('e.%1$s = %2$s.%1$s', $linkField, $temporaryValueTableName), $columnValueNames ); - $allColumns = array_merge($allColumns, $columnValueNames); + $allColumns[] = $columnValueNames; } } - $sql = $select->insertFromSelect($temporaryFlatTableName, $allColumns, false); + $sql = $select->insertFromSelect($temporaryFlatTableName, array_merge(...$allColumns), false); $this->_connection->query($sql); } @@ -319,7 +332,7 @@ protected function _updateTemporaryTableByStoreValues( $linkField = $this->getMetadataPool()->getMetadata(ProductInterface::class)->getLinkField(); foreach ($tables as $tableName => $columns) { foreach ($columns as $attribute) { - /* @var $attribute \Magento\Eav\Model\Entity\Attribute */ + /* @var $attribute Attribute */ $attributeCode = $attribute->getAttributeCode(); if ($attribute->getBackend()->getType() != 'static') { $joinCondition = sprintf('t.%s = e.%s', $linkField, $linkField) . @@ -328,7 +341,7 @@ protected function _updateTemporaryTableByStoreValues( ' AND t.store_id = ' . $storeId . ' AND t.value IS NOT NULL'; - /** @var $select \Magento\Framework\DB\Select */ + /** @var $select Select */ $select = $this->_connection->select() ->joinInner( ['e' => $this->resource->getTableName('catalog_product_entity')], @@ -340,7 +353,9 @@ protected function _updateTemporaryTableByStoreValues( [$attributeCode => 't.value'] ); if (!empty($changedIds)) { - $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); + $select->where( + $this->_connection->quoteInto('et.entity_id IN (?)', $changedIds, \Zend_Db::INT_TYPE) + ); } $sql = $select->crossUpdateFromSelect(['et' => $temporaryFlatTableName]); $this->_connection->query($sql); @@ -363,7 +378,13 @@ protected function _updateTemporaryTableByStoreValues( [$columnName => $columnValue] )->where($columnValue . ' IS NOT NULL'); if (!empty($changedIds)) { - $select->where($this->_connection->quoteInto('et.entity_id IN (?)', $changedIds)); + $select->where( + $this->_connection->quoteInto( + 'et.entity_id IN (?)', + $changedIds, + \Zend_Db::INT_TYPE + ) + ); } $sql = $select->crossUpdateFromSelect(['et' => $temporaryFlatTableName]); $this->_connection->query($sql); @@ -386,13 +407,13 @@ protected function _getTemporaryTableName($tableName) /** * Get metadata pool * - * @return \Magento\Framework\EntityManager\MetadataPool + * @return MetadataPool */ private function getMetadataPool() { if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\EntityManager\MetadataPool::class); + $this->metadataPool = ObjectManager::getInstance() + ->get(MetadataPool::class); } return $this->metadataPool; } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Table/Builder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Table/Builder.php index fb9c8aace8d7d..23eaf7d7b2010 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Table/Builder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/Table/Builder.php @@ -6,7 +6,7 @@ namespace Magento\Catalog\Model\Indexer\Product\Flat\Table; /** - * Class Builder + * Build table structure based on provided columns */ class Builder implements BuilderInterface { 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 0897ae0d74c0b..c14ea4bc363f8 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php @@ -9,7 +9,7 @@ use Magento\Store\Model\Store; /** - * Class TableBuilder + * Prepare temporary tables structure for product flat indexer * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -347,7 +347,9 @@ protected function _fillTemporaryTable( } if (!empty($changedIds)) { - $select->where($this->_connection->quoteInto('e.entity_id IN (?)', $changedIds)); + $select->where( + $this->_connection->quoteInto('e.entity_id IN (?)', $changedIds, \Zend_Db::INT_TYPE) + ); } $sql = $select->insertFromSelect($temporaryTableName, $columns, true); @@ -355,7 +357,9 @@ protected function _fillTemporaryTable( if (count($valueColumns) > 1) { if (!empty($changedIds)) { - $selectValue->where($this->_connection->quoteInto('e.entity_id IN (?)', $changedIds)); + $selectValue->where( + $this->_connection->quoteInto('e.entity_id IN (?)', $changedIds, \Zend_Db::INT_TYPE) + ); } $sql = $selectValue->insertFromSelect($temporaryValueTableName, $valueColumns, true); $this->_connection->query($sql); diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php index 41e72ecf880a5..f3a4b322e29df 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -3,13 +3,31 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Model\Indexer\Product\Price; +use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice; use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; +use Magento\Directory\Model\Currency; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Indexer\DimensionalIndexerInterface; +use Magento\Framework\Search\Request\Dimension; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; /** * Abstract action reindex class @@ -26,48 +44,48 @@ abstract class AbstractAction protected $_defaultIndexerResource; /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface + * @var AdapterInterface */ protected $_connection; /** * Core config model * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $_config; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** * Currency factory * - * @var \Magento\Directory\Model\CurrencyFactory + * @var CurrencyFactory */ protected $_currencyFactory; /** - * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface + * @var TimezoneInterface */ protected $_localeDate; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $_dateTime; /** - * @var \Magento\Catalog\Model\Product\Type + * @var Type */ protected $_catalogProductType; /** * Indexer price factory * - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory + * @var Factory */ protected $_indexerPriceFactory; @@ -77,12 +95,12 @@ abstract class AbstractAction protected $_indexers; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice + * @var TierPrice */ private $tierPriceIndexResource; /** - * @var \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory + * @var DimensionCollectionFactory */ private $dimensionCollectionFactory; @@ -92,15 +110,15 @@ abstract class AbstractAction private $tableMaintainer; /** - * @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 ScopeConfigInterface $config + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory + * @param TimezoneInterface $localeDate + * @param DateTime $dateTime + * @param Type $catalogProductType + * @param Factory $indexerPriceFactory * @param DefaultPrice $defaultIndexerResource - * @param \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice|null $tierPriceIndexResource + * @param TierPrice|null $tierPriceIndexResource * @param DimensionCollectionFactory|null $dimensionCollectionFactory * @param TableMaintainer|null $tableMaintainer * @SuppressWarnings(PHPMD.NPathComplexity) @@ -108,17 +126,17 @@ abstract class AbstractAction * @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, + ScopeConfigInterface $config, + StoreManagerInterface $storeManager, + CurrencyFactory $currencyFactory, + TimezoneInterface $localeDate, + DateTime $dateTime, + Type $catalogProductType, + Factory $indexerPriceFactory, DefaultPrice $defaultIndexerResource, - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice $tierPriceIndexResource = null, - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory $dimensionCollectionFactory = null, - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer $tableMaintainer = null + TierPrice $tierPriceIndexResource = null, + DimensionCollectionFactory $dimensionCollectionFactory = null, + TableMaintainer $tableMaintainer = null ) { $this->_config = $config; $this->_storeManager = $storeManager; @@ -130,13 +148,13 @@ public function __construct( $this->_defaultIndexerResource = $defaultIndexerResource; $this->_connection = $this->_defaultIndexerResource->getConnection(); $this->tierPriceIndexResource = $tierPriceIndexResource ?? ObjectManager::getInstance()->get( - \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice::class + TierPrice::class ); $this->dimensionCollectionFactory = $dimensionCollectionFactory ?? ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory::class + DimensionCollectionFactory::class ); $this->tableMaintainer = $tableMaintainer ?? ObjectManager::getInstance()->get( - \Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer::class + TableMaintainer::class ); } @@ -152,7 +170,7 @@ abstract public function execute($ids); * Synchronize data between index storage and original storage * * @param array $processIds - * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction + * @return AbstractAction * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @deprecated 102.0.6 Used only for backward compatibility for indexer, which not support indexation by dimensions */ @@ -182,14 +200,14 @@ protected function _syncData(array $processIds = []) /** * Prepare website current dates table * - * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction + * @return AbstractAction * - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws LocalizedException + * @throws NoSuchEntityException */ protected function _prepareWebsiteDateTable() { - $baseCurrency = $this->_config->getValue(\Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE); + $baseCurrency = $this->_config->getValue(Currency::XML_PATH_CURRENCY_BASE); $select = $this->getConnection()->select()->from( ['cw' => $this->_defaultIndexerResource->getTable('store_website')], @@ -204,7 +222,7 @@ protected function _prepareWebsiteDateTable() $data = []; foreach ($this->getConnection()->fetchAll($select) as $item) { - /** @var $website \Magento\Store\Model\Website */ + /** @var $website Website */ $website = $this->_storeManager->getWebsite($item['website_id']); if ($website->getBaseCurrencyCode() != $baseCurrency) { @@ -220,7 +238,7 @@ protected function _prepareWebsiteDateTable() $rate = 1; } - /** @var $store \Magento\Store\Model\Store */ + /** @var $store Store */ $store = $this->_storeManager->getStore($item['store_id']); if ($store) { $timestamp = $this->_localeDate->scopeTimeStamp($store); @@ -248,11 +266,11 @@ protected function _prepareWebsiteDateTable() * Prepare tier price index table * * @param int|array $entityIds the entity ids limitation - * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction + * @return AbstractAction */ protected function _prepareTierPriceIndex($entityIds = null) { - $this->tierPriceIndexResource->reindexEntity((array) $entityIds); + $this->tierPriceIndexResource->reindexEntity((array)$entityIds); return $this; } @@ -262,9 +280,9 @@ protected function _prepareTierPriceIndex($entityIds = null) * * @param bool $fullReindexAction * - * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface[] + * @return PriceInterface[] * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function getTypeIndexers($fullReindexAction = false) { @@ -301,16 +319,16 @@ public function getTypeIndexers($fullReindexAction = false) * Retrieve Price indexer by Product Type * * @param string $productTypeId - * @return \Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\PriceInterface + * @return PriceInterface * - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\LocalizedException + * @throws InputException + * @throws LocalizedException */ protected function _getIndexer($productTypeId) { $this->getTypeIndexers(); if (!isset($this->_indexers[$productTypeId])) { - throw new \Magento\Framework\Exception\InputException(__('Unsupported product type "%1".', $productTypeId)); + throw new InputException(__('Unsupported product type "%1".', $productTypeId)); } return $this->_indexers[$productTypeId]; } @@ -335,7 +353,7 @@ protected function _insertFromTable($sourceTable, $destTable, $where = null) $select, $destTable, $targetColumns, - \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + AdapterInterface::INSERT_ON_DUPLICATE ); $this->getConnection()->query($query); } @@ -357,9 +375,9 @@ protected function _emptyTable($table) * @param array $changedIds * @return array Affected ids * - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws InputException + * @throws LocalizedException + * @throws NoSuchEntityException */ protected function _reindexRows($changedIds = []) { @@ -407,7 +425,7 @@ protected function _reindexRows($changedIds = []) } /** - * Delete Index data + * Delete Index data index for list of entities * * @param array $entityIds * @return void @@ -418,7 +436,7 @@ private function deleteIndexData(array $entityIds) $select = $this->getConnection()->select()->from( ['index_price' => $this->tableMaintainer->getMainTableByDimensions($dimensions)], null - )->where('index_price.entity_id IN (?)', $entityIds); + )->where('index_price.entity_id IN (?)', $entityIds, \Zend_Db::INT_TYPE); $query = $select->deleteFromSelect('index_price'); $this->getConnection()->query($query); } @@ -476,7 +494,7 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) * * This method is used during both partial and full reindex to identify the table. * - * @param \Magento\Framework\Search\Request\Dimension[] $dimensions + * @param Dimension[] $dimensions * * @return string */ @@ -529,7 +547,7 @@ private function getProductsTypes(array $changedIds = []) ['entity_id', 'type_id'] ); if ($changedIds) { - $select->where('entity_id IN (?)', $changedIds); + $select->where('entity_id IN (?)', $changedIds, \Zend_Db::INT_TYPE); } $pairs = $this->getConnection()->fetchPairs($select); @@ -575,7 +593,7 @@ private function getParentProductsTypes(array $productsIds) /** * Get connection * - * @return \Magento\Framework\DB\Adapter\AdapterInterface + * @return AdapterInterface */ private function getConnection() { diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/AttributeSetFinder.php b/app/code/Magento/Catalog/Model/Product/Attribute/AttributeSetFinder.php index 4df67d1c01f16..b8da7452b09ce 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/AttributeSetFinder.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/AttributeSetFinder.php @@ -27,7 +27,7 @@ public function __construct(CollectionFactory $productCollectionFactory) } /** - * {@inheritdoc} + * @inheritdoc */ public function findAttributeSetIdsByProductIds(array $productIds) { @@ -37,7 +37,7 @@ public function findAttributeSetIdsByProductIds(array $productIds) ->getSelect() ->reset(Select::COLUMNS) ->columns(ProductInterface::ATTRIBUTE_SET_ID) - ->where('entity_id IN (?)', $productIds) + ->where('entity_id IN (?)', $productIds, \Zend_Db::INT_TYPE) ->group(ProductInterface::ATTRIBUTE_SET_ID); $result = $collection->getConnection()->fetchCol($select); return $result; diff --git a/app/code/Magento/Catalog/Model/Product/Price/PricePersistence.php b/app/code/Magento/Catalog/Model/Product/Price/PricePersistence.php index e3085f7cdefe3..ecab88c9c7e03 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/PricePersistence.php +++ b/app/code/Magento/Catalog/Model/Product/Price/PricePersistence.php @@ -6,8 +6,16 @@ namespace Magento\Catalog\Model\Product\Price; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ProductIdLocatorInterface; +use Magento\Catalog\Model\ResourceModel\Attribute; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\CouldNotDeleteException; +use Magento\Framework\Exception\CouldNotSaveException; + /** - * Price persistence. + * Class responsibly for persistence of prices. */ class PricePersistence { @@ -19,24 +27,24 @@ class PricePersistence private $table = 'catalog_product_entity_decimal'; /** - * @var \Magento\Catalog\Model\ResourceModel\Attribute + * @var Attribute */ private $attributeResource; /** - * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface + * @var ProductAttributeRepositoryInterface */ private $attributeRepository; /** - * @var \Magento\Catalog\Model\ProductIdLocatorInterface + * @var ProductIdLocatorInterface */ private $productIdLocator; /** * Metadata pool. * - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ private $metadataPool; @@ -64,17 +72,17 @@ class PricePersistence /** * PricePersistence constructor. * - * @param \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource - * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository - * @param \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param Attribute $attributeResource + * @param ProductAttributeRepositoryInterface $attributeRepository + * @param ProductIdLocatorInterface $productIdLocator + * @param MetadataPool $metadataPool * @param string $attributeCode */ public function __construct( - \Magento\Catalog\Model\ResourceModel\Attribute $attributeResource, - \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, - \Magento\Catalog\Model\ProductIdLocatorInterface $productIdLocator, - \Magento\Framework\EntityManager\MetadataPool $metadataPool, + Attribute $attributeResource, + ProductAttributeRepositoryInterface $attributeRepository, + ProductIdLocatorInterface $productIdLocator, + MetadataPool $metadataPool, $attributeCode = '' ) { $this->attributeResource = $attributeResource; @@ -97,7 +105,7 @@ public function get(array $skus) ->select() ->from($this->attributeResource->getTable($this->table)); return $this->attributeResource->getConnection()->fetchAll( - $select->where($this->getEntityLinkField() . ' IN (?)', $ids) + $select->where($this->getEntityLinkField() . ' IN (?)', $ids, \Zend_Db::INT_TYPE) ->where('attribute_id = ?', $this->getAttributeId()) ); } @@ -107,7 +115,7 @@ public function get(array $skus) * * @param array $prices * @return void - * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws CouldNotSaveException */ public function update(array $prices) { @@ -127,7 +135,7 @@ public function update(array $prices) $connection->commit(); } catch (\Exception $e) { $connection->rollBack(); - throw new \Magento\Framework\Exception\CouldNotSaveException( + throw new CouldNotSaveException( __('Could not save Prices.'), $e ); @@ -139,7 +147,7 @@ public function update(array $prices) * * @param array $skus * @return void - * @throws \Magento\Framework\Exception\CouldNotDeleteException + * @throws CouldNotDeleteException */ public function delete(array $skus) { @@ -159,7 +167,7 @@ public function delete(array $skus) $connection->commit(); } catch (\Exception $e) { $connection->rollBack(); - throw new \Magento\Framework\Exception\CouldNotDeleteException( + throw new CouldNotDeleteException( __('Could not delete Prices'), $e ); @@ -209,10 +217,10 @@ private function retrieveAffectedIds(array $skus) $affectedIds = []; foreach ($this->productIdLocator->retrieveProductIdsBySkus($skus) as $productIds) { - $affectedIds = array_merge($affectedIds, array_keys($productIds)); + $affectedIds[] = array_keys($productIds); } - return array_unique($affectedIds); + return array_unique(array_merge([], ...$affectedIds)); } /** @@ -222,7 +230,7 @@ private function retrieveAffectedIds(array $skus) */ public function getEntityLinkField() { - return $this->metadataPool->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) + return $this->metadataPool->getMetadata(ProductInterface::class) ->getLinkField(); } } diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPricePersistence.php b/app/code/Magento/Catalog/Model/Product/Price/TierPricePersistence.php index fd247d2ce9e32..65b1aec3b4817 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPricePersistence.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPricePersistence.php @@ -56,7 +56,7 @@ public function get(array $ids) { $select = $this->tierpriceResource->getConnection()->select()->from($this->tierpriceResource->getMainTable()); return $this->tierpriceResource->getConnection()->fetchAll( - $select->where($this->getEntityLinkField() . ' IN (?)', $ids) + $select->where($this->getEntityLinkField() . ' IN (?)', $ids, \Zend_Db::INT_TYPE) ); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php index 3946be32184ec..c71225b4fc67f 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/AbstractResource.php @@ -147,7 +147,7 @@ protected function _getLoadAttributesSelect($object, $table) ->select() ->from(['attr_table' => $table], []) ->where("attr_table.{$this->getLinkField()} = ?", $object->getData($this->getLinkField())) - ->where('attr_table.store_id IN (?)', $storeIds); + ->where('attr_table.store_id IN (?)', $storeIds, \Zend_Db::INT_TYPE); if ($setId) { $select->join( @@ -562,7 +562,11 @@ public function getAttributeRawValue($entityId, $attribute, $store) if ($typedAttributes) { foreach ($typedAttributes as $table => $_attributes) { $defaultJoinCondition = [ - $connection->quoteInto('default_value.attribute_id IN (?)', array_keys($_attributes)), + $connection->quoteInto( + 'default_value.attribute_id IN (?)', + array_keys($_attributes), + \Zend_Db::INT_TYPE + ), "default_value.{$this->getLinkField()} = e.{$this->getLinkField()}", 'default_value.store_id = 0', ]; @@ -589,7 +593,11 @@ public function getAttributeRawValue($entityId, $attribute, $store) 'store_value.attribute_id' ); $joinCondition = [ - $connection->quoteInto('store_value.attribute_id IN (?)', array_keys($_attributes)), + $connection->quoteInto( + 'store_value.attribute_id IN (?)', + array_keys($_attributes), + \Zend_Db::INT_TYPE + ), "store_value.{$this->getLinkField()} = e.{$this->getLinkField()}", 'store_value.store_id = :store_id', ]; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 298ca059c572e..917aafb643b47 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -13,13 +13,13 @@ namespace Magento\Catalog\Model\ResourceModel; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Category\Product\Processor; +use Magento\Catalog\Setup\CategorySetup; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityManager; -use Magento\Catalog\Setup\CategorySetup; use Magento\Framework\EntityManager\MetadataPool; -use Magento\Catalog\Api\Data\ProductInterface; /** * Resource model for category entity @@ -666,7 +666,8 @@ public function findWhereAttributeIs($entityIdsFilter, $attribute, $expectedValu 'ci.value = :value' )->where( 'ce.entity_id IN (?)', - $entityIdsFilter + $entityIdsFilter, + \Zend_Db::INT_TYPE ); $this->entitiesWhereAttributesIs[$entityIdsFilterHash][$attribute->getId()][$expectedValue] = $this->getConnection()->fetchCol($selectEntities, $bind); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php index cd1b8c8924552..759866de4b49d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php @@ -294,7 +294,7 @@ protected function _loadNodes($parentNode = null, $recursionLevel = 0, $storeId $inactiveCategories = $this->getInactiveCategoryIds(); if (!empty($inactiveCategories)) { - $select->where('main_table.entity_id NOT IN (?)', $inactiveCategories); + $select->where('main_table.entity_id NOT IN (?)', $inactiveCategories, \Zend_Db::INT_TYPE); } // Allow extensions to modify select (e.g. add custom category attributes to select) @@ -681,7 +681,8 @@ public function getAnchorsAbove(array $filterIds, $storeId = 0) 1 )->where( 'entity_id IN (?)', - $filterIds + $filterIds, + \Zend_Db::INT_TYPE ); return $this->getConnection()->fetchCol($select); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php index 3a0d47fe573fb..02fdb8270791d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php @@ -170,10 +170,12 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) ['e.entity_id'] )->where( "e.entity_id IN (?)", - array_keys($this->_itemsById) + array_keys($this->_itemsById), + \Zend_Db::INT_TYPE )->where( 't_d.attribute_id IN (?)', - $attributeIds + $attributeIds, + \Zend_Db::INT_TYPE )->joinLeft( ['t_s' => $table], implode(' AND ', $joinCondition), @@ -192,10 +194,12 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) ['e.entity_id'] )->where( "e.entity_id IN (?)", - array_keys($this->_itemsById) + array_keys($this->_itemsById), + \Zend_Db::INT_TYPE )->where( 'attribute_id IN (?)', - $attributeIds + $attributeIds, + \Zend_Db::INT_TYPE )->where( 'store_id = ?', $this->getDefaultStoreId() diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index c3f1ee28b19fe..b174e4beb6353 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -227,12 +227,16 @@ public function getWebsiteIds($product) */ public function getWebsiteIdsByProductIds($productIds) { + if (!is_array($productIds) || empty($productIds)) { + return []; + } $select = $this->getConnection()->select()->from( $this->getProductWebsiteTable(), ['product_id', 'website_id'] )->where( 'product_id IN (?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); $productsWebsites = []; foreach ($this->getConnection()->fetchAll($select) as $productInfo) { @@ -357,7 +361,7 @@ private function deleteSelectedEntityAttributeRows(DataObject $product, array $a $entityId = $product->getData($entityIdField); foreach ($backendTables as $backendTable => $attributes) { $connection = $this->getConnection(); - $where = $connection->quoteInto('attribute_id IN (?)', $attributes); + $where = $connection->quoteInto('attribute_id IN (?)', $attributes, \Zend_Db::INT_TYPE); $where .= $connection->quoteInto(" AND {$entityIdField} = ?", $entityId); $connection->delete($backendTable, $where); } @@ -450,6 +454,7 @@ public function getAvailableInCategories($object) // fetching all parent IDs, including those are higher on the tree $entityId = (int)$object->getEntityId(); if (!isset($this->availableCategoryIdsCache[$entityId])) { + $unionTables = []; foreach ($this->_storeManager->getStores() as $store) { $unionTables[] = $this->getAvailableInCategoriesSelect( $entityId, @@ -594,7 +599,8 @@ public function getProductsSku(array $productIds) ['entity_id', 'sku'] )->where( 'entity_id IN (?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); return $this->getConnection()->fetchAll($select); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 0cc3090100e8b..7dbfe0d5fccea 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -858,7 +858,8 @@ protected function doAddWebsiteNamesToResult() ['name'] )->where( 'product_website.product_id IN (?)', - array_keys($productWebsites) + array_keys($productWebsites), + \Zend_Db::INT_TYPE )->where( 'website.website_id > ?', 0 @@ -1358,7 +1359,7 @@ public function addCountToCategories($categoryCollection) $anchorStmt = clone $select; $anchorStmt->limit(); //reset limits - $anchorStmt->where('count_table.category_id IN (?)', $isAnchor); + $anchorStmt->where('count_table.category_id IN (?)', $isAnchor, \Zend_Db::INT_TYPE); $productCounts += $this->getConnection()->fetchPairs($anchorStmt); $anchorStmt = null; } @@ -1366,7 +1367,7 @@ public function addCountToCategories($categoryCollection) $notAnchorStmt = clone $select; $notAnchorStmt->limit(); //reset limits - $notAnchorStmt->where('count_table.category_id IN (?)', $isNotAnchor); + $notAnchorStmt->where('count_table.category_id IN (?)', $isNotAnchor, \Zend_Db::INT_TYPE); $notAnchorStmt->where('count_table.is_parent = 1'); $productCounts += $this->getConnection()->fetchPairs($notAnchorStmt); $notAnchorStmt = null; @@ -2165,7 +2166,7 @@ public function addCategoryIds() $select = $this->getConnection()->select(); $select->from($this->_productCategoryTable, ['product_id', 'category_id']); - $select->where('product_id IN (?)', $ids); + $select->where('product_id IN (?)', $ids, \Zend_Db::INT_TYPE); $data = $this->getConnection()->fetchAll($select); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index a9741cd8e1ec7..ef274b1bef55e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -487,7 +487,8 @@ public function getProductImages($product, $storeIds) $product->getData($this->metadata->getLinkField()) )->where( 'store_id IN (?)', - $storeIds + $storeIds, + \Zend_Db::INT_TYPE )->where( 'attribute_code IN (?)', ['small_image', 'thumbnail', 'image'] 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 747b06266cce0..578e3099a2fde 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 @@ -775,7 +775,7 @@ protected function _movePriceDataToIndexTable($entityIds = null) $select = $connection->select()->from($table, $columns); if ($entityIds !== null) { - $select->where('entity_id in (?)', count($entityIds) > 0 ? $entityIds : 0); + $select->where('entity_id in (?)', count($entityIds) > 0 ? $entityIds : 0, \Zend_Db::INT_TYPE); } $query = $select->insertFromSelect($this->getIdxTable(), [], false); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php index a866c1eaa413f..aa66978fa0036 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/TierPrice.php @@ -193,7 +193,7 @@ private function getTierPriceSelect(bool $isAllWebsites, bool $isAllCustomerGrou [] ); if (!empty($entityIds)) { - $select->where('entity.entity_id IN (?)', $entityIds); + $select->where('entity.entity_id IN (?)', $entityIds, \Zend_Db::INT_TYPE); } $this->joinWebsites($select, $isAllWebsites); $this->joinCustomerGroups($select, $isAllCustomerGroups); diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php index f1d4552cf37f0..bca919e700364 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Link/Product/Collection.php @@ -6,12 +6,32 @@ namespace Magento\Catalog\Model\ResourceModel\Product\Link\Product; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Catalog\Model\Indexer\Product\Flat\State; use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Link as LinkModel; +use Magento\Catalog\Model\Product\OptionFactory; use Magento\Catalog\Model\ResourceModel\Category; +use Magento\Catalog\Model\ResourceModel\Helper; use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\Catalog\Model\ResourceModel\Url; use Magento\Customer\Api\GroupManagementInterface; +use Magento\Customer\Model\Session; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\EntityFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Indexer\DimensionFactory; +use Magento\Framework\Module\Manager; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\Validator\UniversalFactory; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** * Catalog product linked products collection @@ -26,14 +46,14 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection /** * Store product model * - * @var \Magento\Catalog\Model\Product + * @var Product */ protected $_product; /** * Store product link model * - * @var \Magento\Catalog\Model\Product\Link + * @var LinkModel */ protected $_linkModel; @@ -71,25 +91,25 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection /** * Collection constructor. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Framework\App\ResourceConnection $resource - * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory - * @param \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper - * @param \Magento\Framework\Validator\UniversalFactory $universalFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Module\Manager $moduleManager - * @param \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory - * @param \Magento\Catalog\Model\ResourceModel\Url $catalogUrl - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Magento\Customer\Model\Session $customerSession - * @param \Magento\Framework\Stdlib\DateTime $dateTime + * @param LoggerInterface $logger + * @param FetchStrategyInterface $fetchStrategy + * @param ManagerInterface $eventManager + * @param Config $eavConfig + * @param ResourceConnection $resource + * @param EntityFactory $eavEntityFactory + * @param Helper $resourceHelper + * @param UniversalFactory $universalFactory + * @param StoreManagerInterface $storeManager + * @param Manager $moduleManager + * @param State $catalogProductFlatState + * @param ScopeConfigInterface $scopeConfig + * @param OptionFactory $productOptionFactory + * @param Url $catalogUrl + * @param TimezoneInterface $localeDate + * @param Session $customerSession + * @param DateTime $dateTime * @param GroupManagementInterface $groupManagement - * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection + * @param AdapterInterface|null $connection * @param ProductLimitationFactory|null $productLimitationFactory * @param MetadataPool|null $metadataPool * @param TableMaintainer|null $tableMaintainer @@ -101,25 +121,25 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, - \Psr\Log\LoggerInterface $logger, - \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Eav\Model\Config $eavConfig, - \Magento\Framework\App\ResourceConnection $resource, - \Magento\Eav\Model\EntityFactory $eavEntityFactory, - \Magento\Catalog\Model\ResourceModel\Helper $resourceHelper, - \Magento\Framework\Validator\UniversalFactory $universalFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Module\Manager $moduleManager, - \Magento\Catalog\Model\Indexer\Product\Flat\State $catalogProductFlatState, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Catalog\Model\Product\OptionFactory $productOptionFactory, - \Magento\Catalog\Model\ResourceModel\Url $catalogUrl, - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Magento\Customer\Model\Session $customerSession, - \Magento\Framework\Stdlib\DateTime $dateTime, + LoggerInterface $logger, + FetchStrategyInterface $fetchStrategy, + ManagerInterface $eventManager, + Config $eavConfig, + ResourceConnection $resource, + EntityFactory $eavEntityFactory, + Helper $resourceHelper, + UniversalFactory $universalFactory, + StoreManagerInterface $storeManager, + Manager $moduleManager, + State $catalogProductFlatState, + ScopeConfigInterface $scopeConfig, + OptionFactory $productOptionFactory, + Url $catalogUrl, + TimezoneInterface $localeDate, + Session $customerSession, + DateTime $dateTime, GroupManagementInterface $groupManagement, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + AdapterInterface $connection = null, ProductLimitationFactory $productLimitationFactory = null, MetadataPool $metadataPool = null, TableMaintainer $tableMaintainer = null, @@ -166,10 +186,10 @@ public function __construct( /** * Declare link model and initialize type attributes join * - * @param \Magento\Catalog\Model\Product\Link $linkModel + * @param LinkModel $linkModel * @return $this */ - public function setLinkModel(\Magento\Catalog\Model\Product\Link $linkModel) + public function setLinkModel(LinkModel $linkModel) { $this->_linkModel = $linkModel; if ($linkModel->getLinkTypeId()) { @@ -202,10 +222,10 @@ public function getLinkModel() /** * Initialize collection parent product and add limitation join * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return $this */ - public function setProduct(\Magento\Catalog\Model\Product $product) + public function setProduct(Product $product) { $this->_product = $product; if ($product && $product->getId()) { @@ -239,7 +259,11 @@ public function addExcludeProductFilter($products) $products = [$products]; } $this->_hasLinkFilter = true; - $this->getSelect()->where('links.linked_product_id NOT IN (?)', $products); + $this->getSelect()->where( + 'links.linked_product_id NOT IN (?)', + $products, + \Zend_Db::INT_TYPE + ); } return $this; } @@ -257,7 +281,11 @@ public function addProductFilter($products) $products = [$products]; } $identifierField = $this->getLinkField(); - $this->getSelect()->where("product_entity_table.$identifierField IN (?)", $products); + $this->getSelect()->where( + "product_entity_table.$identifierField IN (?)", + $products, + \Zend_Db::INT_TYPE + ); $this->_hasLinkFilter = true; } @@ -319,10 +347,18 @@ protected function _joinLinks() $linkField = $this->getLinkField(); if ($this->productIds) { if ($this->_isStrongMode) { - $this->getSelect()->where('links.product_id in (?)', $this->productIds); + $this->getSelect()->where( + 'links.product_id in (?)', + $this->productIds, + \Zend_Db::INT_TYPE + ); } else { $joinType = 'joinLeft'; - $joinCondition[] = $connection->quoteInto('links.product_id in (?)', $this->productIds); + $joinCondition[] = $connection->quoteInto( + 'links.product_id in (?)', + $this->productIds, + \Zend_Db::INT_TYPE + ); } if (count($this->productIds) === 1) { $this->addFieldToFilter( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Website.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Website.php index 771f781678e44..eee5106579255 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Website.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Website.php @@ -125,7 +125,8 @@ public function getWebsites($productIds) ['product_id', 'website_id'] )->where( 'product_id IN (?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ); $rowset = $this->getConnection()->fetchAll($select); diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartButtonActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartButtonActionGroup.xml new file mode 100644 index 0000000000000..3d240a21afc28 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickAddToCartButtonActionGroup.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="StorefrontClickAddToCartButtonActionGroup"> + <annotations> + <description>Click "Add to Cart" button.</description> + </annotations> + <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <waitForPageLoad stepKey="waitAddToCart"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml index c35e775152ac9..c94bca1ca5c13 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategorySidebarTreeSection.xml @@ -12,7 +12,7 @@ <element name="collapseAll" type="button" selector=".tree-actions a:first-child"/> <element name="expandAll" type="button" selector=".tree-actions a:last-child"/> <element name="categoryHighlighted" type="text" selector="//div[@id='store.menu']//span[contains(text(),'{{name}}')]/ancestor::li" parameterized="true" timeout="30"/> - <element name="categoryNotHighlighted" type="text" selector="ul[id=\'ui-id-2\'] li[class~=\'active\']" timeout="30"/> + <element name="categoryNotHighlighted" type="text" selector="[id=\'store.menu\'] ul li.active" timeout="30"/> <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="//li/ul/li[@class='x-tree-node']/div/a/span[contains(text(), '{{name}}')]" parameterized="true"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml index 4e86f14611c24..201affacd9adb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridFilterSection.xml @@ -38,5 +38,6 @@ <element name="storeViewDropdown" type="text" selector="//select[@name='store_id']/option[contains(.,'{{storeView}}')]" parameterized="true"/> <element name="inputByCodeRangeFrom" type="input" selector="input.admin__control-text[name='{{code}}[from]']" parameterized="true"/> <element name="inputByCodeRangeTo" type="input" selector="input.admin__control-text[name='{{code}}[to]']" parameterized="true"/> + <element name="storeViewOptions" type="text" selector=".admin__data-grid-outer-wrap select[name='store_id'] > option[value='{{value}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml index 1c937637ad823..a7dd622c56a7f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontFooterSection.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontFooterSection"> <element name="switchStoreButton" type="button" selector="#switcher-store-trigger"/> + <element name="storeViewOptionNumber" type="button" selector="//div[@class='actions dropdown options switcher-options active']//ul//li[{{var1}}]//a" parameterized="true"/> <element name="storeLink" type="button" selector="//ul[@class='dropdown switcher-dropdown']//a[contains(text(),'{{var1}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml index 900b3f6cd2f1c..852353300d090 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminConfigDefaultCategoryLayoutFromConfigurationSettingTest.xml @@ -24,8 +24,7 @@ <actionGroup ref="RestoreLayoutSetting" stepKey="sampleActionGroup"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{WebConfigurationPage.url}}" stepKey="navigateToWebConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOpenWebConfigurationPageActionGroup" stepKey="navigateToWebConfigurationPage"/> <conditionalClick stepKey="expandDefaultLayouts" selector="{{WebSection.DefaultLayoutsTab}}" dependentSelector="{{WebSection.CheckIfTabExpand}}" visible="true"/> <waitForElementVisible selector="{{DefaultLayoutsSection.categoryLayout}}" stepKey="waitForDefaultCategoryLayout"/> <seeOptionIsSelected selector="{{DefaultLayoutsSection.categoryLayout}}" userInput="No layout updates" stepKey="seeNoLayoutUpdatesSelected"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml index dfa289f18711b..500c95d1120f3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml @@ -76,8 +76,7 @@ <argument name="tags" value=""/> </actionGroup> <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="openIndexManagementPage"/> <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> <!--Verify Category In Store Front--> <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml index d0c40ec276abb..2394b41502f84 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -76,9 +76,7 @@ <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="openIndexManagementPage"/> <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> <!--Verify Category In Store Front--> <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml index 8256661f8c525..35e53273aebf2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -77,9 +77,7 @@ <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="openIndexManagementPage"/> <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> <!--Verify Category In Store Front--> <amOnPage url="/$$category.name$$.html" stepKey="openCategoryPage1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml index 7317f2f7214f0..ac2e86a572455 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminConfigDefaultProductLayoutFromConfigurationSettingTest.xml @@ -24,8 +24,7 @@ <actionGroup ref="RestoreLayoutSetting" stepKey="sampleActionGroup"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{WebConfigurationPage.url}}" stepKey="navigateToWebConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOpenWebConfigurationPageActionGroup" stepKey="navigateToWebConfigurationPage"/> <conditionalClick stepKey="expandDefaultLayouts" selector="{{WebSection.DefaultLayoutsTab}}" dependentSelector="{{WebSection.CheckIfTabExpand}}" visible="true"/> <waitForElementVisible selector="{{DefaultLayoutsSection.productLayout}}" stepKey="DefaultProductLayout"/> <selectOption selector="{{DefaultLayoutsSection.productLayout}}" userInput="3 columns" stepKey="select3ColumnsLayout"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml index 059a3a321b16a..ac9c0206f4e24 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml @@ -25,8 +25,7 @@ <createData entity="_defaultCategory" stepKey="createSecondCategory"/> <!-- Switch "Category Product" and "Product Category" indexers to "Update by Schedule" mode --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="onIndexManagement"/> - <waitForPageLoad stepKey="waitForManagementPage"/> + <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="onIndexManagement"/> <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCategoryProduct"> <argument name="indexerValue" value="catalog_category_product"/> @@ -38,8 +37,7 @@ <after> <!-- Switch "Category Product" and "Product Category" indexers to "Update by Save" mode --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="onIndexManagement"/> - <waitForPageLoad stepKey="waitForManagementPage"/> + <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="onIndexManagement"/> <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCategoryProduct"> <argument name="indexerValue" value="catalog_category_product"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml index fea4436446da2..bfa80c2e24b48 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -20,8 +20,14 @@ </annotations> <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <!-- Should wait a bit for filters really cleared because waitForPageLoad does not wait for javascripts to be finished --> + <!-- Without this test will fail sometimes --> + <wait time="5" stepKey="waitFilterReallyCleared"/> + <reloadPage stepKey="reloadPage"/> </before> <after> @@ -31,11 +37,11 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}?filters[name]=$createSimpleProduct.name$" stepKey="navigateToProductGridWithFilters"/> + <amOnPage url="{{AdminProductIndexPage.url}}?filters[sku]=$createSimpleProduct.sku$" stepKey="navigateToProductGridWithFilters"/> <waitForPageLoad stepKey="waitForProductGrid"/> <see selector="{{AdminProductGridSection.productGridNameProduct($createSimpleProduct.name$)}}" userInput="$createSimpleProduct.name$" stepKey="seeProduct"/> <waitForElementVisible selector="{{AdminProductGridFilterSection.enabledFilters}}" stepKey="waitForEnabledFilters"/> <seeElement selector="{{AdminProductGridFilterSection.enabledFilters}}" stepKey="seeEnabledFilters"/> - <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="Name: $createSimpleProduct.name$" stepKey="seeProductNameFilter"/> + <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="SKU: $createSimpleProduct.sku$" stepKey="seeProductNameFilter"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml index b6c508df121df..208b588493112 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml @@ -89,9 +89,7 @@ <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToLoad"/> + <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="openIndexManagementPage"/> <see stepKey="seeCategoryIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> <!--Verify Product In Store Front--> <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToStorefrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml index 8eb7813b6203e..a688dea47a0c4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -71,9 +71,7 @@ <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> + <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="openIndexManagementPage"/> <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="Ready"/> <!--Verify Category In Store Front--> <amOnPage url="/$$createCategory.name$$.html" stepKey="openCategoryPage1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml index 94f8d0e1dc523..27a834833ed76 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml @@ -76,9 +76,7 @@ <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> <argument name="tags" value=""/> </actionGroup> - <!--Open Index Management Page --> - <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> - <waitForPageLoad stepKey="waitForIndexPageToLoad"/> + <actionGroup ref="AdminOpenIndexManagementPageActionGroup" stepKey="openIndexManagementPage"/> <see stepKey="seeIndexStatus" selector="{{AdminIndexManagementSection.indexerStatus('Category Flat Data')}}" userInput="READY"/> <!--Verify Category In Store Front--> <amOnPage url="{{SimpleSubCategory.name}}.html" stepKey="goToStorefrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyComparedAtWebsiteLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyComparedAtWebsiteLevelTest.xml deleted file mode 100644 index 7ec5fea49f64b..0000000000000 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyComparedAtWebsiteLevelTest.xml +++ /dev/null @@ -1,108 +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="StoreFrontRecentlyComparedAtWebsiteLevelTest"> - <annotations> - <features value="Catalog"/> - <stories value="Recently Compared Product"/> - <title value="Recently Compared Product at website level"/> - <description value="Recently Compared Products widget appears on a page immediately after adding product to compare"/> - <severity value="MAJOR"/> - <testCaseId value="MC-33099"/> - <useCaseId value="MC-32763"/> - <group value="catalog"/> - <group value="widget"/> - <skip> - <issueId value="MC-34091"/> - </skip> - </annotations> - <before> - <!-- Set Stores > Configurations > Catalog > Recently Viewed/Compared Products > Show for Current = Website --> - <magentoCLI command="config:set {{RecentlyViewedProductScopeWebsite.path}} {{RecentlyViewedProductScopeWebsite.value}}" stepKey="setRecentlyViewedComparedProductsScopeToWebsite"/> - <!--Create Simple Products and Category --> - <createData entity="SimpleSubCategory" stepKey="createCategory"/> - <createData entity="SimpleProduct" stepKey="createSimpleProductToCompareFirst"> - <requiredEntity createDataKey="createCategory"/> - </createData> - <createData entity="SimpleProduct" stepKey="createSimpleProductToCompareSecond"> - <requiredEntity createDataKey="createCategory"/> - </createData> - <createData entity="SimpleProduct" stepKey="createSimpleProductNotVisibleFirst"> - <requiredEntity createDataKey="createCategory"/> - </createData> - <createData entity="SimpleProduct" stepKey="createSimpleProductNotVisibleSecond"> - <requiredEntity createDataKey="createCategory"/> - </createData> - <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <!-- Login as admin --> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!-- Create product widget --> - <actionGroup ref="AdminCreateRecentlyProductsWidgetActionGroup" stepKey="createRecentlyComparedProductsWidget"> - <argument name="widget" value="RecentlyComparedProductsWidget"/> - </actionGroup> - </before> - <after> - <!-- Reset Stores > Configurations > Catalog > Recently Viewed/Compared Products > Show for Current = Website--> - <magentoCLI command="config:set {{RecentlyViewedProductScopeWebsite.path}} {{RecentlyViewedProductScopeWebsite.value}}" stepKey="setRecentlyViewedComparedProductsScopeToDefault"/> - <!-- Delete Products and Category --> - <deleteData createDataKey="createSimpleProductToCompareFirst" stepKey="deleteSimpleProductToCompareFirst"/> - <deleteData createDataKey="createSimpleProductToCompareSecond" stepKey="deleteSimpleProductToCompareSecond"/> - <deleteData createDataKey="createSimpleProductNotVisibleFirst" stepKey="deleteSimpleProductNotVisibleFirst"/> - <deleteData createDataKey="createSimpleProductNotVisibleSecond" stepKey="deleteSimpleProductNotVisibleSecond"/> - <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <!-- Customer Logout --> - <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutFromCustomer"/> - <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <!-- Delete product widget --> - <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteRecentlyComparedProductsWidget"> - <argument name="widget" value="RecentlyComparedProductsWidget"/> - </actionGroup> - <!-- Logout Admin --> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - </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"/> - <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="openCategoryPage"/> - <!--Add to compare Simple Product and Simple Product 2--> - <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addSimpleProduct1ToCompare" > - <argument name="productVar" value="$createSimpleProductToCompareFirst$"/> - </actionGroup> - <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addSimpleProduct2ToCompare" > - <argument name="productVar" value="$createSimpleProductToCompareSecond$"/> - </actionGroup> - <!--The Compare Products widget displays Simple Product 1 and Simple Product 2--> - <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="checkSimpleProduct1InCompareSidebar"> - <argument name="productVar" value="$createSimpleProductToCompareFirst$"/> - </actionGroup> - <actionGroup ref="StorefrontCheckCompareSidebarProductActionGroup" stepKey="checkSimpleProduct2InCompareSidebar"> - <argument name="productVar" value="$createSimpleProductToCompareSecond$"/> - </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--> - <waitForPageLoad stepKey="waitForRecentlyComparedWidgetLoad"/> - <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProduct1ExistInRecentlyComparedWidget"> - <argument name="product" value="$createSimpleProductToCompareFirst$"/> - </actionGroup> - <actionGroup ref="StorefrontAssertProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProduct2ExistInRecentlyComparedWidget"> - <argument name="product" value="$createSimpleProductToCompareSecond$"/> - </actionGroup> - <!--The Recently Compared widget not displays Simple Product 3 and Simple Product 4--> - <actionGroup ref="StorefrontAssertNotExistProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProduct3NotExistInRecentlyComparedWidget"> - <argument name="product" value="$createSimpleProductNotVisibleFirst$"/> - </actionGroup> - <actionGroup ref="StorefrontAssertNotExistProductInRecentlyComparedWidgetActionGroup" stepKey="checkSimpleProduct4NotExistInRecentlyComparedWidget"> - <argument name="product" value="$createSimpleProductNotVisibleSecond$"/> - </actionGroup> - </test> -</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml index 74264149cf1cb..9731b66209df0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontForthLevelCategoryTest.xml @@ -34,6 +34,9 @@ <deleteData createDataKey="category3" stepKey="deleteCategory3"/> <deleteData createDataKey="category2" stepKey="deleteCategory2"/> <deleteData createDataKey="category1" stepKey="deleteCategory1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="full_page"/> + </actionGroup> </after> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnStorefrontPage"/> <moveMouseOver diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml index 0dccc409a1032..164701fa5bc6d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontSpecialPriceForDifferentTimezonesForWebsitesTest.xml @@ -37,16 +37,14 @@ </after> <!--Set timezone for default config--> - <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfig"/> - <waitForPageLoad stepKey="waitForConfigPage"/> + <actionGroup ref="AdminOpenGeneralConfigurationPageActionGroup" stepKey="goToGeneralConfig"/> <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="AdminOpenGeneralConfigurationPageActionGroup" stepKey="goToGeneralConfig1"/> <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup"> <argument name="website" value="_defaultWebsite"/> </actionGroup> @@ -80,15 +78,13 @@ </assertEquals> <!--Reset timezone--> - <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset"/> - <waitForPageLoad stepKey="waitForConfigPageReset"/> + <actionGroup ref="AdminOpenGeneralConfigurationPageActionGroup" stepKey="goToGeneralConfigReset"/> <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="AdminOpenGeneralConfigurationPageActionGroup" stepKey="goToGeneralConfigReset1"/> <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreViewActionGroup1"> <argument name="website" value="_defaultWebsite"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml index e7ba97ad36785..ce04b377300f8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -58,9 +58,9 @@ <argument name="categoryName" value="$$categoryN.name$$, $$categoryM.name$$"/> </actionGroup> - <wait stepKey="waitBeforeRunCronIndex" time="30"/> + <wait stepKey="waitBeforeRunCronIndex" time="60"/> <magentoCLI stepKey="runCronIndex" command="cron:run --group=index"/> - <wait stepKey="waitAfterRunCronIndex" time="60"/> + <wait stepKey="waitAfterRunCronIndex" time="120"/> </before> <after> <!-- Change "Category Products" and "Product Categories" indexers to "Update on Save" mode --> @@ -108,6 +108,8 @@ <argument name="categoryName" value="$$categoryK.name$$"/> </actionGroup> + <wait stepKey="waitAfterAssignCategoryK" time="60"/> + <!-- Unassign category M from Product B --> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="amOnEditCategoryPageB"> <argument name="productId" value="$$productB.id$$"/> @@ -147,8 +149,9 @@ <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductInCategoryN"/> <!-- Run cron --> - <wait stepKey="waitBeforeRunMagentoCron" time="30"/> + <wait stepKey="waitBeforeRunMagentoCron" time="60"/> <magentoCLI stepKey="runMagentoCron" command="cron:run --group=index"/> + <wait stepKey="waitAfterRunMagentoCron" time="90"/> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> @@ -213,8 +216,9 @@ <see userInput="$$productC.name$$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryN"/> <!-- Run Cron once to reindex product changes --> - <wait stepKey="waitBeforeRunCronIndexAfterProductAssignToCategory" time="30"/> + <wait stepKey="waitBeforeRunCronIndexAfterProductAssignToCategory" time="60"/> <magentoCLI stepKey="runCronIndexAfterProductAssignToCategory" command="cron:run --group=index"/> + <wait stepKey="waitAfterRunCronIndexAfterProductAssignToCategory" time="90"/> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php index aa29972c91a62..c606b7537cc44 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php @@ -396,6 +396,14 @@ public function testGetWidth() $this->assertEquals($data['width'], $this->helper->getWidth()); } + /** + * Check initBaseFile without properties - product + */ + public function testGetUrlWithOutProduct() + { + $this->assertNull($this->helper->getUrl()); + } + /** * @param array $data * @dataProvider getHeightDataProvider diff --git a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php index d3c8c406ee34d..2aa30fb18fdf4 100644 --- a/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php +++ b/app/code/Magento/Catalog/ViewModel/Product/Breadcrumbs.php @@ -71,7 +71,7 @@ public function __construct( public function getCategoryUrlSuffix() { return $this->scopeConfig->getValue( - static::XML_PATH_CATEGORY_URL_SUFFIX, + self::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE ); } @@ -84,7 +84,7 @@ public function getCategoryUrlSuffix() public function isCategoryUsedInProductUrl(): bool { return $this->scopeConfig->isSetFlag( - static::XML_PATH_PRODUCT_USE_CATEGORIES, + self::XML_PATH_PRODUCT_USE_CATEGORIES, ScopeInterface::SCOPE_STORE ); } diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml index a0aa48fb76b13..ddd66a5bf04bd 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -154,7 +154,7 @@ default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> - <column xsi:type="text" name="value" nullable="true" comment="Value"/> + <column xsi:type="mediumtext" name="value" nullable="true" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> @@ -408,7 +408,7 @@ default="0" comment="Store ID"/> <column xsi:type="int" name="entity_id" unsigned="true" nullable="false" identity="false" default="0" comment="Entity ID"/> - <column xsi:type="text" name="value" nullable="true" comment="Value"/> + <column xsi:type="mediumtext" name="value" nullable="true" comment="Value"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="value_id"/> </constraint> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/product-ui-select.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/product-ui-select.js index fb7ea7a5bcd69..c04daf07db3dd 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/components/product-ui-select.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/product-ui-select.js @@ -3,6 +3,9 @@ * See COPYING.txt for license details. */ +/** + * @deprecated see Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js + */ define([ 'Magento_Ui/js/form/element/ui-select', 'jquery', diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml index c805941fa0272..6a47978f1e5c6 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml @@ -98,7 +98,8 @@ $_helper = $block->getData('outputHelper'); <?= $block->getBlockHtml('formkey') ?> <button type="submit" title="<?= $escaper->escapeHtmlAttr(__('Add to Cart')) ?>" - class="action tocart primary"> + class="action tocart primary" + disabled> <span><?= $escaper->escapeHtml(__('Add to Cart')) ?></span> </button> </form> 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 7d3e4b3280473..fbce6691fd66a 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 @@ -34,6 +34,7 @@ define([ if (this.options.bindSubmit) { this._bindSubmit(); } + $(this.options.addToCartButtonSelector).attr('disabled', false); }, /** diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index c4f9bc26ee9f3..9ed36098ab6eb 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -180,4 +180,12 @@ <argument name="templateFilterModel" xsi:type="string">Magento\Widget\Model\Template\FilterEmulate</argument> </arguments> </type> + <type name="Magento\WishlistGraphQl\Model\Resolver\Type\WishlistItemType"> + <arguments> + <argument name="supportedTypes" xsi:type="array"> + <item name="simple" xsi:type="string">SimpleWishlistItem</item> + <item name="virtual" xsi:type="string">VirtualWishlistItem</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index 35f2c767b3e1e..35067a6cb99af 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -492,3 +492,9 @@ type StoreConfig @doc(description: "The type contains information about a store catalog_default_sort_by : String @doc(description: "Default Sort By.") root_category_id: Int @doc(description: "The ID of the root category") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\RootCategoryId") } + +type SimpleWishlistItem implements WishlistItemInterface @doc(description: "A simple product wish list Item") { +} + +type VirtualWishlistItem implements WishlistItemInterface @doc(description: "A virtual product wish list item") { +} diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php index 35231b8460b19..2ad7ca9f14963 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/ProductPriceIndexFilter.php @@ -105,7 +105,7 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = } if (!empty($entityIds)) { - $select->where('stock_item.product_id in (?)', $entityIds); + $select->where('stock_item.product_id in (?)', $entityIds, \Zend_Db::INT_TYPE); } $select->group('stock_item.product_id'); diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php index b3fa07479a712..f1cef90fc68ca 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php @@ -118,7 +118,7 @@ private function getProductStockStatuses(array $productIds) 'cpr.parent_id = cpe.' . $linkField, ['parent_id' => 'cpe.entity_id'] ) - ->where('product_id IN (?)', $productIds) + ->where('product_id IN (?)', $productIds, \Zend_Db::INT_TYPE) ->where('stock_id = ?', Stock::DEFAULT_STOCK_ID) ->where('website_id = ?', $this->stockConfiguration->getDefaultScopeId()); diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php index 1f6f3a16ac617..f994bb8fe26a1 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php @@ -138,18 +138,18 @@ public function lockProductsStock(array $productIds, $websiteId) $itemIds = []; $preSelect = $this->getConnection()->select()->from($itemTable, 'item_id') ->where('website_id = ?', $websiteId) - ->where('product_id IN(?)', $productIds); + ->where('product_id IN(?)', $productIds, \Zend_Db::INT_TYPE); foreach ($this->getConnection()->query($preSelect)->fetchAll() as $item) { $itemIds[] = (int)$item['item_id']; } $select = $this->getConnection()->select()->from(['si' => $itemTable]) - ->where('item_id IN (?)', $itemIds) + ->where('item_id IN (?)', $itemIds, \Zend_Db::INT_TYPE) ->forUpdate(true); $productTable = $this->getTable('catalog_product_entity'); $selectProducts = $this->getConnection()->select()->from(['p' => $productTable], []) - ->where('entity_id IN (?)', $productIds) + ->where('entity_id IN (?)', $productIds, \Zend_Db::INT_TYPE) ->columns( [ 'product_id' => 'entity_id', diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index 25bc0a0ce899e..02e443d09b228 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -5,9 +5,18 @@ */ namespace Magento\CatalogInventory\Model\ResourceModel\Stock; +use Magento\Catalog\Model\Product; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\Stock; +use Magento\Eav\Model\Config; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Select; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; +use Magento\Store\Model\WebsiteFactory; /** * CatalogInventory Stock Status per website Resource Model @@ -19,12 +28,12 @@ * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html * @since 100.0.2 */ -class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Status extends AbstractDb { /** * Store model manager * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface * @deprecated 100.1.0 */ protected $_storeManager; @@ -32,12 +41,12 @@ class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb /** * Website model factory * - * @var \Magento\Store\Model\WebsiteFactory + * @var WebsiteFactory */ protected $_websiteFactory; /** - * @var \Magento\Eav\Model\Config + * @var Config */ protected $eavConfig; @@ -47,18 +56,18 @@ class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb private $stockConfiguration; /** - * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Store\Model\WebsiteFactory $websiteFactory - * @param \Magento\Eav\Model\Config $eavConfig + * @param Context $context + * @param StoreManagerInterface $storeManager + * @param WebsiteFactory $websiteFactory + * @param Config $eavConfig * @param string $connectionName - * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration + * @param StockConfigurationInterface $stockConfiguration */ public function __construct( - \Magento\Framework\Model\ResourceModel\Db\Context $context, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Store\Model\WebsiteFactory $websiteFactory, - \Magento\Eav\Model\Config $eavConfig, + Context $context, + StoreManagerInterface $storeManager, + WebsiteFactory $websiteFactory, + Config $eavConfig, $connectionName = null, $stockConfiguration = null ) { @@ -128,6 +137,7 @@ public function saveProductStatus( /** * Retrieve product status + * * Return array as key product id, value - stock status * * @param int[] $productIds @@ -151,13 +161,14 @@ public function getProductsStockStatuses($productIds, $websiteId, $stockId = Sto /** * Retrieve websites and default stores + * * Return array as key website_id, value store_id * * @return array */ public function getWebsiteStores() { - /** @var \Magento\Store\Model\Website $website */ + /** @var Website $website */ $website = $this->_websiteFactory->create(); return $this->getConnection()->fetchPairs($website->getDefaultStoresSelect(false)); } @@ -186,6 +197,7 @@ public function getProductsType($productIds) /** * Retrieve Product part Collection array + * * Return array as key product id, value product type * * @param int $lastEntityId @@ -207,12 +219,12 @@ public function getProductCollection($lastEntityId = 0, $limit = 1000) /** * Add stock status to prepare index select * - * @param \Magento\Framework\DB\Select $select - * @param \Magento\Store\Model\Website $website + * @param Select $select + * @param Website $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @return Status */ - public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, \Magento\Store\Model\Website $website) + public function addStockStatusToSelect(Select $select, Website $website) { $websiteId = $this->getWebsiteId($website->getId()); $select->joinLeft( @@ -225,6 +237,8 @@ public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, \Ma } /** + * Add Stock information to Product Collection + * * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection * @param bool $isFilterInStock * @return \Magento\Catalog\Model\ResourceModel\Product\Collection $collection @@ -289,7 +303,9 @@ public function addIsInStockFilterToCollection($collection) } /** - * @param \Magento\Store\Model\Website $websiteId + * Get website with fallback to default + * + * @param Website $websiteId * @return int */ private function getWebsiteId($websiteId = null) @@ -303,6 +319,7 @@ private function getWebsiteId($websiteId = null) /** * Retrieve Product(s) status for store + * * Return array where key is a product_id, value - status * * @param int[] $productIds @@ -315,17 +332,17 @@ public function getProductStatus($productIds, $storeId = null) $productIds = [$productIds]; } - $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'status'); + $attribute = $this->eavConfig->getAttribute(Product::ENTITY, 'status'); $attributeTable = $attribute->getBackend()->getTable(); $linkField = $attribute->getEntity()->getLinkField(); $connection = $this->getConnection(); - if ($storeId === null || $storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID) { + if ($storeId === null || $storeId == Store::DEFAULT_STORE_ID) { $select = $connection->select()->from($attributeTable, [$linkField, 'value']) - ->where("{$linkField} IN (?)", $productIds) + ->where("{$linkField} IN (?)", $productIds, \Zend_Db::INT_TYPE) ->where('attribute_id = ?', $attribute->getAttributeId()) - ->where('store_id = ?', \Magento\Store\Model\Store::DEFAULT_STORE_ID); + ->where('store_id = ?', Store::DEFAULT_STORE_ID); $rows = $connection->fetchPairs($select); } else { @@ -337,7 +354,7 @@ public function getProductStatus($productIds, $storeId = null) "t1.{$linkField} = t2.{$linkField} AND t1.attribute_id = t2.attribute_id AND t2.store_id = {$storeId}" )->where( 't1.store_id = ?', - \Magento\Store\Model\Store::DEFAULT_STORE_ID + Store::DEFAULT_STORE_ID )->where( 't1.attribute_id = ?', $attribute->getAttributeId() diff --git a/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php b/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php index 404fc32e0c0d4..90e50538bcba3 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/ProductPriceIndexModifier.php @@ -71,7 +71,7 @@ public function modifyPrice(IndexTableStructure $priceTable, array $entityIds = [] ); if ($entityIds) { - $select->where('i.entity_id IN (?)', $entityIds); + $select->where('i.entity_id IN (?)', $entityIds, \Zend_Db::INT_TYPE); } $finalPrice = $priceTable->getFinalPriceField(); diff --git a/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/ConfigurableProductsProvider.php b/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/ConfigurableProductsProvider.php index dd020114b03ab..1ef0490092b40 100644 --- a/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/ConfigurableProductsProvider.php +++ b/app/code/Magento/CatalogRuleConfigurable/Plugin/CatalogRule/Model/ConfigurableProductsProvider.php @@ -27,12 +27,14 @@ public function __construct(\Magento\Framework\App\ResourceConnection $resource) } /** + * Return list of ID for product variation + * * @param array $ids * @return array */ public function getIds(array $ids) { - $key = md5(json_encode($ids)); + $key = md5(json_encode($ids)); //phpcs:ignore if (!isset($this->productIds[$key])) { $connection = $this->resource->getConnection(); $this->productIds[$key] = $connection->fetchCol( @@ -40,7 +42,7 @@ public function getIds(array $ids) ->select() ->from(['e' => $this->resource->getTableName('catalog_product_entity')], ['e.entity_id']) ->where('e.type_id = ?', \Magento\ConfigurableProduct\Model\Product\Type\Configurable::TYPE_CODE) - ->where('e.entity_id IN (?)', $ids) + ->where('e.entity_id IN (?)', $ids, \Zend_Db::INT_TYPE) ); } return $this->productIds[$key]; 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 ffa7dfd80df0c..8c4690f044764 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -242,7 +242,7 @@ private function getSelectForSearchableProducts( $this->joinAttribute($select, 'status', $storeId, [Status::STATUS_ENABLED]); if ($productIds !== null) { - $select->where('e.entity_id IN (?)', $productIds); + $select->where('e.entity_id IN (?)', $productIds, \Zend_Db::INT_TYPE); } $select->where('e.entity_id > ?', $lastProductId); $select->order('e.entity_id'); @@ -410,7 +410,8 @@ public function getProductAttributes($storeId, array $productIds, array $attribu [$linkField, 'entity_id'] )->where( 'cpe.entity_id IN (?)', - $productIds + $productIds, + \Zend_Db::INT_TYPE ) ); foreach ($attributeTypes as $backendType => $attributeIds) { 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 fa4d9fee415cf..3ce8a96fb5070 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -317,7 +317,7 @@ protected function getProductIdsFromParents(array $entityIds) ->select() ->from(['relation' => $this->getTable('catalog_product_relation')], []) ->distinct(true) - ->where('child_id IN (?)', $entityIds) + ->where('child_id IN (?)', $entityIds, \Zend_Db::INT_TYPE) ->join( ['cpe' => $this->getTable('catalog_product_entity')], 'relation.parent_id = cpe.' . $linkField, diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php index 3614cd9dbf3a9..ad6d37c296012 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php @@ -116,7 +116,8 @@ public function getRelationsByChild($childIds) ['cpe.entity_id'] )->where( 'relation.child_id IN (?)', - $childIds + $childIds, + \Zend_Db::INT_TYPE ); return $connection->fetchCol($select); diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php index e625ccbe51fe3..d37f0f8a5153b 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Search/Collection.php @@ -258,7 +258,8 @@ protected function _getSearchEntityIdsSql($query, $searchOnlyInCurrentStore = tr [] )->where( 't1.attribute_id IN (?)', - $attributeIds + $attributeIds, + \Zend_Db::INT_TYPE )->where( 't1.store_id = ?', 0 @@ -332,7 +333,8 @@ protected function _getSearchInOptionSql($query) 'd.store_id=0' )->where( 'o.attribute_id IN (?)', - $attributeIds + $attributeIds, + \Zend_Db::INT_TYPE )->where( $this->_resourceHelper->getCILike($ifValue, $this->_searchQuery, ['position' => 'any']) ); diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminOpenCatalogSearchTermIndexPageActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminOpenCatalogSearchTermIndexPageActionGroup.xml new file mode 100644 index 0000000000000..e8528d4126376 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminOpenCatalogSearchTermIndexPageActionGroup.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="AdminOpenCatalogSearchTermIndexPageActionGroup"> + <annotations> + <description>Open catalog search term index page.</description> + </annotations> + + <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openCatalogSearchTermIndexPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml index b42313fc14773..26280ed67d183 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml @@ -39,8 +39,7 @@ <deleteData createDataKey="createCategory1" stepKey="deleteCategory1"/> <!-- Delete all search terms --> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> - <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage"/> <comment userInput="Delete all search terms" stepKey="deleteAllSearchTermsComment"/> <actionGroup ref="AdminDeleteAllSearchTermsActionGroup" stepKey="deleteAllSearchTerms"/> @@ -53,8 +52,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage1"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage1"/> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByFirstSearchQuery1"> <argument name="searchQuery" value="$$createProduct1.name$$"/> @@ -67,8 +65,7 @@ <argument name="searchTerm" value="UpdatedSearchTermData1"/> </actionGroup> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage2"/> - <waitForPageLoad stepKey="waitForPageLoad3"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage2"/> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByFirstSearchQuery2"> <argument name="searchQuery" value="{{UpdatedSearchTermData1.query_text}}"/> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.xml new file mode 100644 index 0000000000000..e146506d51a24 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/AdminFillCatalogProductsListWidgetTitleActionGroup.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="AdminFillCatalogProductsListWidgetTitleActionGroup"> + <annotations> + <description>Fill catalog products list title field.</description> + </annotations> + + <arguments> + <argument name="title" type="string" defaultValue=""/> + </arguments> + <waitForElementVisible selector="{{InsertWidgetSection.title}}" stepKey="waitForField"/> + <fillField selector="{{InsertWidgetSection.title}}" userInput="{{title}}" stepKey="fillTitleField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.xml new file mode 100644 index 0000000000000..4505680424471 --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/ActionGroup/StorefrontAssertWidgetTitleActionGroup.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="StorefrontAssertWidgetTitleActionGroup"> + <annotations> + <description>Assert widget title on storefront.</description> + </annotations> + <arguments> + <argument name="title" type="string"/> + </arguments> + + <grabTextFrom selector="{{StorefrontWidgetsSection.widgetProductsGrid}} {{StorefrontWidgetsSection.widgetTitle}}" + stepKey="grabWidgetTitle"/> + <assertEquals stepKey="assertWidgetTitle"> + <actualResult type="string">$grabWidgetTitle</actualResult> + <expectedResult type="string">{{title}}</expectedResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml index 9b40971611d6f..3d8d5ecc1cda9 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Section/CatalogWidgetSection/InsertWidgetSection.xml @@ -19,5 +19,6 @@ <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"/> <element name="categoryTreeWrapper" type="text" selector=".rule-chooser .tree.x-tree"/> + <element name="title" type="text" selector="input[name='parameters[title]']"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutActionGroup.xml new file mode 100644 index 0000000000000..8210fe1df73ba --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutActionGroup.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="AssertStorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutActionGroup"> + <annotations> + <description>Checks if visible password field for unregistered email on checkout page</description> + </annotations> + + <waitForPageLoad stepKey="waitForCheckoutPageLoaded"/> + <dontSeeElement selector="{{StorefrontCheckoutCheckoutCustomerLoginSection.password}}" stepKey="checkIfPasswordVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutErrorMessageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutErrorMessageActionGroup.xml new file mode 100644 index 0000000000000..6db9d9a1f0673 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontAssertCheckoutErrorMessageActionGroup.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="StorefrontAssertCheckoutErrorMessageActionGroup"> + <arguments> + <argument name="message" type="string"/> + </arguments> + + <waitForElementVisible selector="{{CheckoutCartMessageSection.errorMessageText(message)}}" stepKey="assertErrorMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartMessageSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartMessageSection.xml index cf15cdf15cf15..0c7f200e2b5eb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartMessageSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartMessageSection.xml @@ -12,5 +12,6 @@ <element name="successMessage" type="text" selector=".message.message-success.success>div" /> <element name="errorMessage" type="text" selector=".message-error.error.message>div" /> <element name="emptyCartMessage" type="text" selector=".cart-empty>p"/> + <element name="errorMessageText" type="text" selector="//div[contains(@class, 'message-error')]/div[text()='{{var}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index 5a9857f6aaa78..1c9933064154a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -15,6 +15,8 @@ <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="editAddress" type="button" selector="button.action.action-edit-address"/> + <element name="addressDropdown" type="select" selector="[name=billing_address_id]"/> + <element name="addressDropdownSelected" type="select" selector="[name=billing_address_id] option:checked"/> <element name="placeOrderDisabled" type="button" selector="#checkout-payment-method-load button.disabled"/> <element name="update" type="button" selector=".payment-method._active .payment-method-billing-address .action.action-update"/> <element name="guestFirstName" type="input" selector=".payment-method._active .billing-address-form input[name='firstname']"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index 7eae5d0d292d1..e7e8f9f0ef699 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -35,7 +35,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" 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"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml index 12e1a6e9872d3..7c4b18e1aab89 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml @@ -38,7 +38,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" 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"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml index 313f5997e0af0..bf942e70cfa36 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml @@ -40,7 +40,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addProductToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="addProductToCart"/> <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createSimpleProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml index 6a211c3908059..13968964436b4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml @@ -26,7 +26,7 @@ </createData> <!-- Create customer --> - <createData entity="Customer_US_UK_DE" stepKey="createCustomer"/> + <createData entity="Customer_DE_UK_US" stepKey="createCustomer"/> </before> <after> <!-- Admin log out --> @@ -70,7 +70,8 @@ <!-- Change the address --> <click selector="{{CheckoutPaymentSection.editAddress}}" stepKey="editAddress"/> - <waitForElementVisible selector="{{CheckoutShippingSection.addressDropdown}}" stepKey="waitForDropDownToBeVisible"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.addressDropdown}}" stepKey="waitForDropDownToBeVisible"/> + <see selector="{{CheckoutPaymentSection.addressDropdownSelected}}" userInput="{{US_Address_NY.street[0]}}" stepKey="seeDefaultBillingAddressStreet"/> <selectOption selector="{{CheckoutShippingSection.addressDropdown}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="addAddress"/> <!-- Check order summary in checkout --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml index 92150b1e99dd5..9747980801068 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml @@ -83,7 +83,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$simplecategory.name$$)}}" stepKey="onCategoryPage1"/> <waitForPageLoad stepKey="waitForCatalogPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct1"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart1"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="addToCart1"/> <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded1"/> <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$simpleproduct1.name$$ to your shopping cart." stepKey="seeAddedToCartMessage1"/> <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity1"/> @@ -102,7 +102,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$simplecategory.name$$)}}" stepKey="onCategoryPage2"/> <waitForPageLoad stepKey="waitForCatalogPageLoad2"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct2"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart2"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="addToCart2"/> <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded2"/> <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$simpleproduct1.name$$ to your shopping cart." stepKey="seeAddedToCartMessage2"/> <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity2"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml index 732038f24834b..d6f1408c2b66a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -52,7 +52,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" 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"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.xml new file mode 100644 index 0000000000000..28e779f802cde --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutWithCustomerGroupTest.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="StorefrontCustomerCheckoutWithCustomerGroupTest"> + <annotations> + <features value="Customer Checkout"/> + <stories value="Customer checkout with Customer Group assigned"/> + <title value="Place order by Customer with Customer Group assigned"/> + <description value="Customer Group should be assigned to Order when setting Auto Group Assign is enabled for Customer"/> + <testCaseId value="MC-37259"/> + <severity value="MAJOR"/> + <group value="checkout"/> + <group value="customer"/> + </annotations> + <before> + + <magentoCLI command="config:set customer/create_account/auto_group_assign 1" stepKey="enableAutoGroupAssign"/> + + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="AdminUpdateCustomerGroupByEmailActionGroup" stepKey="updateCustomerGroup"> + <argument name="emailAddress" value="$$createCustomer.email$$"/> + <argument name="customerGroup" value="Retail"/> + </actionGroup> + + </before> + <after> + <magentoCLI command="config:set customer/create_account/auto_group_assign 0" stepKey="disableAutoGroupAssign"/> + + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteUsCustomer"/> + <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="resetCustomerFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteSimpleCategory"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="storefrontCustomerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <actionGroup ref="StorefrontAddCategoryProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="CONST.one"/> + </actionGroup> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRate"/> + <actionGroup ref="StorefrontCheckoutForwardFromShippingStepActionGroup" stepKey="goToReview"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyOrder"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="orderNumber"/> + + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="addFilterToGridAndOpenOrder"> + <argument name="orderId" value="{$orderNumber}"/> + </actionGroup> + + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="verifyOrderStatus"/> + <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="Customer" stepKey="verifyAccountInformation"/> + <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="$$createCustomer.email$$" stepKey="verifyCustomerEmail"/> + <see selector="{{AdminOrderDetailsInformationSection.accountInformation}}" userInput="Retail" stepKey="verifyCustomerGroup"/> + <see selector="{{AdminOrderDetailsInformationSection.billingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="verifyBillingAddress"/> + <see selector="{{AdminOrderDetailsInformationSection.shippingAddress}}" userInput="{{US_Address_TX.street[0]}}" stepKey="verifyShippingAddress"/> + <see selector="{{AdminOrderDetailsInformationSection.itemsOrdered}}" userInput="$$createSimpleProduct.name$$" stepKey="verifyProductName"/> + + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml index be5cf143f13dc..b591aefbdc889 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml @@ -40,7 +40,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" 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"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml index 7660df18407d5..15b550657ef60 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -43,7 +43,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" 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"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml index b7c1d7b83e9b7..03323b7b9c855 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml @@ -28,8 +28,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="goToCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="moveMouseOverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="clickAddToCartButton"/> - <waitForPageLoad stepKey="waitForAddToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="clickAddToCartButton"/> <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForAddedToCartSuccessMessage"/> <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$product.name$$ to your shopping cart." stepKey="seeAddedToCartSuccessMessage"/> <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.xml new file mode 100644 index 0000000000000..41b5f734d0096 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest.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="StorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutTest"> + <annotations> + <features value="Checkout"/> + <stories value="Visible password field for unregistered e-mail on Checkout"/> + <title value="Visibility password field for unregistered e-mail on Checkout process"/> + <description value="Guest should not be able to see password field if entered unregistered email"/> + <severity value="MINOR"/> + <group value="checkout"/> + </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="fillUnregisteredEmailFirstAttempt"> + <argument name="email" value="unregistered@email.test"/> + </actionGroup> + <actionGroup ref="AssertStorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutActionGroup" stepKey="checkIfPasswordVisibleAfterFieldFilling"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="reloadCheckoutPage" /> + <actionGroup ref="AssertStorefrontVisiblePasswordFieldForUnregisteredEmailOnCheckoutActionGroup" + stepKey="checkIfPasswordVisibleAfterPageReload"/> + </test> +</tests> 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 index ca3a267c01671..80411fb8eb29d 100644 --- 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 @@ -23,6 +23,9 @@ define([ }, addressOptions = addressList().filter(function (address) { return address.getType() === 'customer-address'; + }), + addressDefaultIndex = addressOptions.findIndex(function (address) { + return address.isDefaultBilling(); }); return Component.extend({ @@ -53,7 +56,8 @@ define([ this._super() .observe('selectedAddress isNewAddressSelected') .observe({ - isNewAddressSelected: !customer.isLoggedIn() || !addressOptions.length + isNewAddressSelected: !customer.isLoggedIn() || !addressOptions.length, + selectedAddress: this.addressOptions[addressDefaultIndex] }); return this; 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 9adfb549a5b1c..8311d97522980 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 @@ -113,6 +113,7 @@ define([ $.when(this.isEmailCheckComplete).done(function () { this.isPasswordVisible(false); + checkoutData.setCheckedEmailValue(''); }.bind(this)).fail(function () { this.isPasswordVisible(true); checkoutData.setCheckedEmailValue(this.email()); @@ -192,6 +193,10 @@ define([ * @returns {Boolean} - initial visibility state. */ resolveInitialPasswordVisibility: function () { + if (checkoutData.getInputFieldEmailValue() !== '' && checkoutData.getCheckedEmailValue() !== '') { + return true; + } + if (checkoutData.getInputFieldEmailValue() !== '') { return checkoutData.getInputFieldEmailValue() === checkoutData.getCheckedEmailValue(); } diff --git a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php index 9efd24e5003ca..d1c6b0fe7956d 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php @@ -102,7 +102,7 @@ protected function _construct() 'type' => 'button' ], 0, - 0, + 100, 'header' ); } 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 29f84e0b2e534..1f991bb47c6fd 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -10,7 +10,6 @@ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Action\HttpPostActionInterface; -use Magento\Framework\App\Filesystem\DirectoryList; /** * Delete image folder. 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 82d200beb6dc9..706718455a523 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/NewFolder.php @@ -65,7 +65,7 @@ public function execute() } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - + return $resultJson->setData($result); } } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php index 3244a7d14f0a3..c7b0752e52181 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/OnInsert.php @@ -1,62 +1,63 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; -class OnInsert extends \Magento\Cms\Controller\Adminhtml\Wysiwyg\Images +use Magento\Backend\App\Action\Context; +use Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\GetInsertImageContent; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Registry; + +class OnInsert extends Images implements HttpPostActionInterface { /** - * @var \Magento\Framework\Controller\Result\RawFactory + * @var RawFactory */ protected $resultRawFactory; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + * @var GetInsertImageContent + */ + private $getInsertImageContent; + + /** + * @param Context $context + * @param Registry $coreRegistry + * @param RawFactory $resultRawFactory + * @param GetInsertImageContent $getInsertImageContent */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Registry $coreRegistry, - \Magento\Framework\Controller\Result\RawFactory $resultRawFactory + Context $context, + Registry $coreRegistry, + RawFactory $resultRawFactory, + ?GetInsertImageContent $getInsertImageContent = null ) { $this->resultRawFactory = $resultRawFactory; parent::__construct($context, $coreRegistry); + $this->getInsertImageContent = $getInsertImageContent ?: $this->_objectManager + ->get(GetInsertImageContent::class); } /** - * Fire when select image + * Return a content (just a link or an html block) for inserting image to the content * - * @return \Magento\Framework\Controller\ResultInterface + * @return ResultInterface */ public function execute() { - $imagesHelper = $this->_objectManager->get(\Magento\Cms\Helper\Wysiwyg\Images::class); - $request = $this->getRequest(); - - $storeId = $request->getParam('store'); - - $filename = $request->getParam('filename'); - $filename = $imagesHelper->idDecode($filename); - - $asIs = $request->getParam('as_is'); - - $forceStaticPath = $request->getParam('force_static_path'); - - $this->_objectManager->get(\Magento\Catalog\Helper\Data::class)->setStoreId($storeId); - $imagesHelper->setStoreId($storeId); - - if ($forceStaticPath) { - $image = parse_url($imagesHelper->getCurrentUrl() . $filename, PHP_URL_PATH); - } else { - $image = $imagesHelper->getImageHtmlDeclaration($filename, $asIs); - } - - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ - $resultRaw = $this->resultRawFactory->create(); - return $resultRaw->setContents($image); + $data = $this->getRequest()->getParams(); + return $this->resultRawFactory->create()->setContents( + $this->getInsertImageContent->execute( + $data['filename'], + $data['force_static_path'], + $data['as_is'], + isset($data['store']) ? (int) $data['store'] : null + ) + ); } } 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 9bad371aa84d7..260755ea7d562 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/Upload.php @@ -74,7 +74,7 @@ public function execute() } /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); - + return $resultJson->setData($response); } } diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContent.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContent.php new file mode 100644 index 0000000000000..305d73fff4dc7 --- /dev/null +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContent.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Wysiwyg\Images; + +use Magento\Catalog\Helper\Data as CatalogHelper; +use Magento\Cms\Helper\Wysiwyg\Images as ImagesHelper; + +class GetInsertImageContent +{ + /** + * @var ImagesHelper + */ + private $imagesHelper; + + /** + * @var CatalogHelper + */ + private $catalogHelper; + + /** + * @param ImagesHelper $imagesHelper + * @param CatalogHelper $catalogHelper + */ + public function __construct(ImagesHelper $imagesHelper, CatalogHelper $catalogHelper) + { + $this->imagesHelper = $imagesHelper; + $this->catalogHelper = $catalogHelper; + } + + /** + * Create a content (just a link or an html block) for inserting image to the content + * + * @param string $encodedFilename + * @param bool $forceStaticPath + * @param bool $renderAsTag + * @param int|null $storeId + * @return string + */ + public function execute( + string $encodedFilename, + bool $forceStaticPath, + bool $renderAsTag, + ?int $storeId = null + ): string { + $filename = $this->imagesHelper->idDecode($encodedFilename); + + $this->catalogHelper->setStoreId($storeId); + $this->imagesHelper->setStoreId($storeId); + + if ($forceStaticPath) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return parse_url($this->imagesHelper->getCurrentUrl() . $filename, PHP_URL_PATH); + } + + return $this->imagesHelper->getImageHtmlDeclaration($filename, $renderAsTag); + } +} diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml index 7e907b5b395a4..68eca3b429e2b 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsPageActionGroup.xml @@ -8,6 +8,9 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminOpenCmsPageActionGroup"> + <annotations> + <description>Open CMS edit page.</description> + </annotations> <arguments> <argument name="page_id" type="string"/> </arguments> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCreateNewCMSPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCreateNewCMSPageActionGroup.xml index 79ce1bc9d8e47..4e19329e9b899 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCreateNewCMSPageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCreateNewCMSPageActionGroup.xml @@ -8,6 +8,10 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminOpenCreateNewCMSPageActionGroup"> + <annotations> + <description>Open create new CMS Page.</description> + </annotations> + <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToCreateNewPage"/> <waitForPageLoad stepKey="waitForNewPagePageLoad"/> </actionGroup> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml index 9163ec4d9f5f8..35d8e692cd460 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml @@ -37,8 +37,7 @@ <magentoCLI command="config:set cms/wysiwyg/editor mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter" stepKey="enableTinyMCE4"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage2"/> - <waitForPageLoad stepKey="wait5"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage2"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle2"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab2" /> <comment userInput="removing deprecated element" stepKey="waitForTinyMCE3"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml index 55c01f3818a19..698f29a28598f 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddVariableToWYSIWYGCMSTest.xml @@ -31,8 +31,7 @@ <fillField selector="{{StoreConfigSection.City}}" userInput="{{_defaultVariable.city}}" stepKey="fillCity" /> <click selector="{{StoreConfigSection.Save}}" stepKey="saveConfig"/> <!--Main test--> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml index 450003db465a8..509e1abe81ef6 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSPageLinkTypeTest.xml @@ -24,8 +24,7 @@ <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml index 633dd4dbc3388..cfb323683dc2c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCMSStaticBlockTypeTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml index 14bdc89cec311..d9ea67491e30a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogCategoryLinkTypeTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="ConfigAdminAccountSharingActionGroup" stepKey="allowAdminShareAccount"/> </before> <!--Main test--> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml index 2b788bc6ca0fd..86f90e0e2a580 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductLinkTypeTest.xml @@ -28,8 +28,7 @@ <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml index 2124206466c2d..dcb4c3dc11f3c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithCatalogProductListTypeTest.xml @@ -30,8 +30,7 @@ <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml index 85ae0380d4b43..6acf8ef18a332 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyComparedProductsTypeTest.xml @@ -28,8 +28,7 @@ <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml index 14182a4c33549..1ec4f7054e8c2 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddWidgetToWYSIWYGWithRecentlyViewedProductsTypeTest.xml @@ -28,8 +28,7 @@ <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> <!--Main test--> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml index 0eac31c891e64..bc159f2309ab8 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminConfigDefaultCMSPageLayoutFromConfigurationSettingTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="RestoreLayoutSetting" stepKey="sampleActionGroup"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{WebConfigurationPage.url}}" stepKey="navigateToWebConfigurationPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOpenWebConfigurationPageActionGroup" stepKey="navigateToWebConfigurationPage"/> <conditionalClick stepKey="expandDefaultLayouts" selector="{{WebSection.DefaultLayoutsTab}}" dependentSelector="{{WebSection.CheckIfTabExpand}}" visible="true" /> <waitForElementVisible selector="{{DefaultLayoutsSection.pageLayout}}" stepKey="DefaultProductLayout" /> <seeOptionIsSelected selector="{{DefaultLayoutsSection.pageLayout}}" userInput="1 column" stepKey="seeOneColumnSelected" /> @@ -34,8 +33,7 @@ <seeOptionIsSelected selector="{{DefaultLayoutsSection.categoryLayout}}" userInput="No layout updates" stepKey="seeNoLayoutUpdatesSelected2" /> <selectOption selector="{{DefaultLayoutsSection.pageLayout}}" userInput="2 columns with right bar" stepKey="selectColumnsWithRightBar"/> <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig" /> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="amOnPagePagesGrid"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="amOnPagePagesGrid"/> <waitForLoadingMaskToDisappear stepKey="wait2" /> <click selector="{{CmsDesignSection.DesignTab}}" stepKey="clickOnDesignTab"/> <waitForElementVisible selector="{{CmsDesignSection.LayoutDropdown}}" stepKey="waitForLayoutDropDown" /> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.xml new file mode 100644 index 0000000000000..bc379ec424fce --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/StoreFrontWidgetTitleWithReservedCharsTest.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="StoreFrontWidgetTitleWithReservedCharsTest"> + <annotations> + <features value="Cms"/> + <stories value="Create a CMS Page via the Admin when widget title contains reserved chairs"/> + <title value="Create CMS Page via the Admin when widget title contains reserved chairs"/> + <description value="See CMS Page title on store front page if titled widget with reserved chairs added"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37419"/> + <group value="Cms"/> + <group value="WYSIWYGDisabled"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="simpleProductWithoutCategory" stepKey="createSimpleProductWithoutCategory"/> + <createData entity="_defaultCmsPage" stepKey="createCmsPage"/> + </before> + <after> + <deleteData createDataKey="createSimpleProductWithoutCategory" stepKey="deleteProduct"/> + <deleteData createDataKey="createCmsPage" stepKey="deleteCmsPage" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Navigate to Page in Admin--> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$createCmsPage$"/> + </actionGroup> + <!--Insert widget--> + <actionGroup ref="AdminInsertWidgetToCmsPageContentActionGroup" stepKey="insertWidgetToCmsPageContent"> + <argument name="widgetType" value="Catalog Products List"/> + </actionGroup> + <!--Fill widget title and save--> + <actionGroup ref="AdminFillCatalogProductsListWidgetTitleActionGroup" stepKey="fillWidgetTitle"> + <argument name="title" value="Tittle }}"/> + </actionGroup> + <actionGroup ref="AdminClickInsertWidgetActionGroup" stepKey="clickInsertWidgetButton"/> + <actionGroup ref="SaveCmsPageActionGroup" stepKey="saveOpenedPage"/> + <!--Verify data on frontend--> + <actionGroup ref="StorefrontGoToCMSPageActionGroup" stepKey="navigateToPageOnStorefront"> + <argument name="identifier" value="$createCmsPage.identifier$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertWidgetTitleActionGroup" stepKey="verifyPageDataOnFrontend"> + <argument name="title" value="Tittle }}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Config/Test/Mftf/Section/AdminEmailToFriendSection.xml b/app/code/Magento/Config/Test/Mftf/Section/AdminEmailToFriendSection.xml new file mode 100644 index 0000000000000..956316ed5cb46 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Section/AdminEmailToFriendSection.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="AdminEmailToFriendSection"> + <element name="DefaultLayoutsTab" type="button" selector=".entry-edit-head-link"/> + <element name="CheckIfTabExpand" type="button" selector=".entry-edit-head-link:not(.open)"/> + <element name="emailTemplate" type="input" selector="#sendfriend_email_template"/> + <element name="allowForGuests" type="input" selector="#sendfriend_email_allow_guest"/> + <element name="maxRecipients" type="input" selector="#sendfriend_email_max_recipients"/> + <element name="maxPerHour" type="input" selector="#sendfriend_email_max_per_hour"/> + <element name="limitSendingBy" type="input" selector="#sendfriend_email_check_by"/> + </section> +</sections> 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 636ff85d12e24..efd0edd23ad11 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -303,6 +303,11 @@ protected function getOptionPrices() $prices[$product->getId()] = [ + 'baseOldPrice' => [ + 'amount' => $this->localeFormat->getNumber( + $priceInfo->getPrice('regular_price')->getAmount()->getBaseAmount() + ), + ], 'oldPrice' => [ 'amount' => $this->localeFormat->getNumber( $priceInfo->getPrice('regular_price')->getAmount()->getValue() diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php index b8a948d55f11a..492c5de55ad7f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable/Variations/Prices.php @@ -39,6 +39,9 @@ public function getFormattedPrices(\Magento\Framework\Pricing\PriceInfo\Base $pr $finalPrice = $priceInfo->getPrice('final_price'); return [ + 'baseOldPrice' => [ + 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getBaseAmount()), + ], 'oldPrice' => [ 'amount' => $this->localeFormat->getNumber($regularPrice->getAmount()->getValue()), ], 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 9080314126ee1..6031ab6f8f8ae 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 @@ -221,7 +221,7 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar ['le.entity_id', 'customer_group_id', 'website_id'] ); if ($entityIds !== null) { - $select->where('le.entity_id IN (?)', $entityIds); + $select->where('le.entity_id IN (?)', $entityIds, \Zend_Db::INT_TYPE); } $this->tableMaintainer->insertFromSelect($select, $temporaryOptionsTableName, []); } 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 feffd22a0fb3d..9d779d9704c29 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable.php @@ -173,7 +173,8 @@ public function getChildrenIds($parentId, $required = true) [] )->where( 'p.entity_id IN (?)', - $parentId + $parentId, + \Zend_Db::INT_TYPE ); $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 0ced38c4a6923..e4b9acbde3030 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 @@ -41,8 +41,8 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * Product instance * * @var \Magento\Catalog\Model\Product - * @deprecated 100.3.0 Now collection supports fetching options for multiple products. This field will be set to first - * element of products array. + * @deprecated 100.3.0 Now collection supports fetching options for multiple products. + * This field will be set to first element of products array. */ protected $_product; @@ -286,7 +286,8 @@ protected function _loadLabels() ['use_default' => $useDefaultCheck, 'label' => $labelCheck] )->where( 'def.product_super_attribute_id IN (?)', - array_keys($this->_items) + array_keys($this->_items), + \Zend_Db::INT_TYPE )->where( 'def.store_id = ?', 0 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 ae591474cd13e..cefd4b815d729 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 @@ -8,7 +8,7 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product; /** - * Class Collection + * Collection of configurable product variation * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -85,7 +85,7 @@ protected function _renderFilters() $parentIds[] = $product->getData($metadata->getLinkField()); } - $this->getSelect()->where('link_table.parent_id in (?)', $parentIds); + $this->getSelect()->where('link_table.parent_id in (?)', $parentIds, \Zend_Db::INT_TYPE); return $this; } diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index cf0e99f7c45c0..37c129dc3bbde 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,6 +13,7 @@ <element name="selectableProductOptions" type="select" selector="#attribute{{var1}} option:not([disabled])" 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="tierPriceBlock" type="block" selector="div[data-role='tier-price-block']"/> <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']"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml index 1491081a82ee4..4de01b0c9d14e 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml @@ -48,7 +48,7 @@ <!--Add tier price in one product --> <createData entity="tierProductPrice" stepKey="addTierPrice"> - <requiredEntity createDataKey="createFirstSimpleProduct" /> + <requiredEntity createDataKey="createFirstSimpleProduct" /> </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> @@ -109,5 +109,8 @@ <expectedResult type="string">Buy {{tierProductPrice.quantity}} for ${{tierProductPrice.price}} each and save 27%</expectedResult> <actualResult type="variable">tierPriceText</actualResult> </assertEquals> + <seeElement selector="{{StorefrontProductInfoMainSection.tierPriceBlock}}" stepKey="seeTierPriceBlock"/> + <selectOption userInput="$$createConfigProductAttributeOptionTwo.option[store_labels][1][label]$$" selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" stepKey="selectOption2"/> + <dontSeeElement selector="{{StorefrontProductInfoMainSection.tierPriceBlock}}" stepKey="dontSeeTierPriceBlock"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml index 2ca8bbc9feb9d..238f1e107c11b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductAddToCartTest.xml @@ -39,8 +39,7 @@ <waitForPageLoad stepKey="wait1"/> <click selector="{{StorefrontCategoryMainSection.modeListButton}}" stepKey="clickListView"/> <waitForPageLoad stepKey="wait2"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="clickAddToCart"/> - <waitForPageLoad stepKey="wait3"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="clickAddToCart"/> <grabFromCurrentUrl stepKey="grabUrl"/> <assertStringContainsString stepKey="assertUrl"> <expectedResult type="string">{{_defaultProduct.urlKey}}</expectedResult> 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 33b7cbe35b391..08279c55c5b30 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 @@ -254,8 +254,11 @@ public function cacheKeyProvider(): array * @param string|null $priceCurrency * @param int|null $customerGroupId */ - public function testGetCacheKeyInfo(array $expected, ?string $priceCurrency = null, ?int $customerGroupId = null) - { + public function testGetCacheKeyInfo( + array $expected, + ?string $priceCurrency = null, + ?int $customerGroupId = null + ): void { $storeMock = $this->getMockBuilder(StoreInterface::class) ->setMethods(['getCurrentCurrency']) ->getMockForAbstractClass(); @@ -282,7 +285,7 @@ public function testGetCacheKeyInfo(array $expected, ?string $priceCurrency = nu /** * Check that getJsonConfig() method returns expected value */ - public function testGetJsonConfig() + public function testGetJsonConfig(): void { $productId = 1; $amount = 10.50; @@ -347,6 +350,9 @@ public function testGetJsonConfig() ->with($priceInfoMock) ->willReturn( [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], @@ -386,6 +392,9 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): 'currencyFormat' => '%s', 'optionPrices' => [ $productId => [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], @@ -403,12 +412,15 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): ], ], 'msrpPrice' => [ - 'amount' => null , + 'amount' => null, ] ], ], 'priceFormat' => [], 'prices' => [ + 'baseOldPrice' => [ + 'amount' => $amount, + ], 'oldPrice' => [ 'amount' => $amount, ], @@ -434,7 +446,7 @@ private function getExpectedArray($productId, $amount, $priceQty, $percentage): * @param MockObject $productMock * @return MockObject */ - private function getProductTypeMock(MockObject $productMock) + private function getProductTypeMock(MockObject $productMock): MockObject { $currencyMock = $this->getMockBuilder(Currency::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php index aa546ae7ad728..c6aa9dc8e20c0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Product/Type/Configurable/Variations/PricesTest.php @@ -36,9 +36,12 @@ protected function setUp(): void ); } - public function testGetFormattedPrices() + public function testGetFormattedPrices(): void { $expected = [ + 'baseOldPrice' => [ + 'amount' => 1000 + ], 'oldPrice' => [ 'amount' => 500 ], @@ -60,8 +63,8 @@ public function testGetFormattedPrices() $this->localeFormatMock->expects($this->atLeastOnce()) ->method('getNumber') - ->withConsecutive([500], [1000], [500]) - ->will($this->onConsecutiveCalls(500, 1000, 500)); + ->withConsecutive([1000], [500], [1000], [500]) + ->will($this->onConsecutiveCalls(1000, 500, 1000, 500)); $this->assertEquals($expected, $this->model->getFormattedPrices($priceInfoMock)); } 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 68e7d146d33e0..814b5de71a8f7 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 @@ -219,7 +219,6 @@ define([ _.each(tmpData, function (row, index) { path = this.dataScope + '.' + this.index + '.' + (this.startIndex + index); row.attributes = $('<i></i>').text(row.attributes).html(); - row.sku = row.sku; this.source.set(path, row); }, this); @@ -227,11 +226,11 @@ define([ this.parsePagesData(data); // Render - dataCount = data.length; + dataCount = tmpData.length; elemsCount = this.elems().length; if (dataCount > elemsCount) { - this.getChildItems().each(function (elemData, index) { + tmpData.each(function (elemData, index) { this.addChild(elemData, this.startIndex + index); }, this); } else { @@ -243,6 +242,15 @@ define([ this.generateAssociatedProducts(); }, + /** + * Set initial property to records data + * + * @returns {Object} Chainable. + */ + setInitialProperty: function () { + return this; + }, + /** * Parsed data * 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 f705b6a95987c..00030be74324f 100644 --- a/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js +++ b/app/code/Magento/ConfigurableProduct/view/frontend/web/js/configurable.js @@ -740,21 +740,19 @@ define([ * @private */ _displayTierPriceBlock: function (optionId) { - var options, tierPriceHtml; + var tierPrices = typeof optionId != 'undefined' && this.options.spConfig.optionPrices[optionId].tierPrices; - if (typeof optionId != 'undefined' && - this.options.spConfig.optionPrices[optionId].tierPrices != [] // eslint-disable-line eqeqeq - ) { - options = this.options.spConfig.optionPrices[optionId]; + if (_.isArray(tierPrices) && tierPrices.length > 0) { if (this.options.tierPriceTemplate) { - tierPriceHtml = mageTemplate(this.options.tierPriceTemplate, { - 'tierPrices': options.tierPrices, - '$t': $t, - 'currencyFormat': this.options.spConfig.currencyFormat, - 'priceUtils': priceUtils - }); - $(this.options.tierPriceBlockSelector).html(tierPriceHtml).show(); + $(this.options.tierPriceBlockSelector).html( + mageTemplate(this.options.tierPriceTemplate, { + 'tierPrices': tierPrices, + '$t': $t, + 'currencyFormat': this.options.spConfig.currencyFormat, + 'priceUtils': priceUtils + }) + ).show(); } } else { $(this.options.tierPriceBlockSelector).hide(); diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Wishlist/ChildSku.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Wishlist/ChildSku.php new file mode 100644 index 0000000000000..84decab81c96a --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Wishlist/ChildSku.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Wishlist; + +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; + +/** + * Fetches the simple child sku of configurable product + */ +class ChildSku implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$value['model'] instanceof Product) { + throw new LocalizedException(__('"itemModel" should be a "%instance" instance', [ + 'instance' => Product::class + ])); + } + + /** @var Product $product */ + $product = $value['model']; + $optionProduct = $product->getCustomOption('simple_product')->getProduct(); + + return $optionProduct->getSku(); + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Wishlist/ConfigurableOptions.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Wishlist/ConfigurableOptions.php new file mode 100644 index 0000000000000..6fcb3e118e5f1 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Wishlist/ConfigurableOptions.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Wishlist; + +use Magento\Catalog\Helper\Product\Configuration; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +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; + +/** + * Fetches the selected configurable options + */ +class ConfigurableOptions implements ResolverInterface +{ + /** + * @var Configuration + */ + private $configurationHelper; + + /** + * @param Configuration $configurationHelper + */ + public function __construct( + Configuration $configurationHelper + ) { + $this->configurationHelper = $configurationHelper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$value['itemModel'] instanceof ItemInterface) { + throw new LocalizedException(__('"itemModel" should be a "%instance" instance', [ + 'instance' => ItemInterface::class + ])); + } + + /** @var ItemInterface $item */ + $item = $value['itemModel']; + $result = []; + + foreach ($this->configurationHelper->getOptions($item) as $option) { + $result[] = [ + 'id' => $option['option_id'], + 'option_label' => $option['label'], + 'value_id' => $option['option_value'], + 'value_label' => $option['value'], + ]; + } + + return $result; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index f82bb0dbd4d91..808ca62f7e149 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -36,4 +36,11 @@ </argument> </arguments> </type> + <type name="Magento\WishlistGraphQl\Model\Resolver\Type\WishlistItemType"> + <arguments> + <argument name="supportedTypes" xsi:type="array"> + <item name="configurable" xsi:type="string">ConfigurableWishlistItem</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 6e85653380acc..257bca11fb5b7 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -68,3 +68,8 @@ type SelectedConfigurableOption { value_id: Int! value_label: String! } + +type ConfigurableWishlistItem implements WishlistItemInterface @doc(description: "A configurable product wish list item"){ + child_sku: String! @doc(description: "The SKU of the simple product corresponding to a set of selected configurable options") @resolver(class: "\\Magento\\ConfigurableProductGraphQl\\Model\\Wishlist\\ChildSku") + configurable_options: [SelectedConfigurableOption!] @resolver(class: "\\Magento\\ConfigurableProductGraphQl\\Model\\Wishlist\\ConfigurableOptions") @doc (description: "An array of selected configurable options") +} diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml index eee9f742a59a4..e9d0c065fd8bf 100644 --- a/app/code/Magento/Contact/view/frontend/templates/form.phtml +++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml @@ -69,8 +69,8 @@ $viewModel = $block->getViewModel(); class="input-text" cols="5" rows="3" - data-validate="{required:true}"><?= $block->escapeHtml($viewModel->getUserComment()) ?> - </textarea> + data-validate="{required:true}" + ><?= $block->escapeHtml($viewModel->getUserComment()) ?></textarea> </div> </div> <?= $block->getChildHtml('form.additional.info') ?> diff --git a/app/code/Magento/Cron/etc/db_schema.xml b/app/code/Magento/Cron/etc/db_schema.xml index f26b6feea3b3b..609b435f8b39c 100644 --- a/app/code/Magento/Cron/etc/db_schema.xml +++ b/app/code/Magento/Cron/etc/db_schema.xml @@ -28,5 +28,9 @@ <column name="scheduled_at"/> <column name="status"/> </index> + <index referenceId="CRON_SCHEDULE_SCHEDULE_ID_STATUS" indexType="btree"> + <column name="schedule_id"/> + <column name="status"/> + </index> </table> </schema> diff --git a/app/code/Magento/Cron/etc/db_schema_whitelist.json b/app/code/Magento/Cron/etc/db_schema_whitelist.json index c8666896627e2..f0d6ebed8290f 100644 --- a/app/code/Magento/Cron/etc/db_schema_whitelist.json +++ b/app/code/Magento/Cron/etc/db_schema_whitelist.json @@ -12,10 +12,11 @@ }, "index": { "CRON_SCHEDULE_JOB_CODE": true, - "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true + "CRON_SCHEDULE_SCHEDULED_AT_STATUS": true, + "CRON_SCHEDULE_SCHEDULE_ID_STATUS": true }, "constraint": { "PRIMARY": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Csp/etc/csp_whitelist.xml b/app/code/Magento/Csp/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..b0cce028ac8b6 --- /dev/null +++ b/app/code/Magento/Csp/etc/csp_whitelist.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="img-src"> + <values> + <value id="data" type="host">data:</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php index ec73ac0cf7aa5..9e7a2b69f20a5 100644 --- a/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php +++ b/app/code/Magento/CurrencySymbol/Block/Adminhtml/System/Currency.php @@ -41,7 +41,14 @@ protected function _prepareLayout() ] ); - $onClick = "setLocation('" . $this->getUrl('adminhtml/system_config/edit/section/currency') . "')"; + $currencyOptionPath = $this->getUrl( + 'adminhtml/system_config/edit', + [ + 'section' => 'currency', + '_fragment' => 'currency_options-link' + ] + ); + $onClick = "setLocation('$currencyOptionPath')"; $this->getToolbar()->addChild( 'options_button', diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.xml new file mode 100644 index 0000000000000..39f37c745998e --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/ActionGroup/AdminNavigateToCurrencyRatesOptionActionGroup.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="AdminNavigateToCurrencyRatesOptionActionGroup"> + <click selector="{{AdminCurrencyRatesSection.options}}" stepKey="clickOptionsButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml index bc80a51c41c47..10f345ec69369 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Section/AdminCurrencyRatesSection.xml @@ -11,6 +11,7 @@ <section name="AdminCurrencyRatesSection"> <element name="import" type="button" selector="//button[@title='Import']"/> <element name="saveCurrencyRates" type="button" selector="//button[@title='Save Currency Rates']"/> + <element name="options" type="button" selector="//button[@title='Options']"/> <element name="oldRate" type="text" selector="//div[contains(@class, 'admin__field-note') and contains(text(), 'Old rate:')]/strong"/> <element name="rateService" type="select" selector="#rate_services"/> <element name="currencyRate" type="input" selector="input[name='rate[{{fistCurrency}}][{{secondCurrency}}]']" parameterized="true"/> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml new file mode 100644 index 0000000000000..4e0eb72df3aa5 --- /dev/null +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyOptionsSystemConfigExpandedTabTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminCurrencyOptionsSystemConfigExpandedTabTest"> + <annotations> + <features value="Expanded tab on Currency Option page"/> + <stories value="Expanded tab"/> + <title value=" Verify the Currency Option tab expands automatically."/> + <description value="Check auto open the collapse on Currency Option page."/> + <severity value="MINOR"/> + <testCaseId value="MC-37425"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToStoresCurrencyRatesPage"> + <argument name="menuUiId" value="{{AdminMenuStores.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuStoresCurrencyCurrencyRates.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminNavigateToCurrencyRatesOptionActionGroup" stepKey="navigateToOptions" /> + <grabAttributeFrom selector="{{CurrencySetupSection.currencyOptions}}" userInput="class" stepKey="grabClass"/> + <assertStringContainsString stepKey="assertClass"> + <actualResult type="string">{$grabClass}</actualResult> + <expectedResult type="string">open</expectedResult> + </assertStringContainsString> + </test> +</tests> \ No newline at end of file diff --git a/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php b/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php index aa7cd06666121..4b86df94b4556 100644 --- a/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php +++ b/app/code/Magento/CurrencySymbol/Test/Unit/Block/Adminhtml/System/CurrencyTest.php @@ -7,15 +7,22 @@ namespace Magento\CurrencySymbol\Test\Unit\Block\Adminhtml\System; +use Magento\Backend\Block\Template\Context; use Magento\Backend\Block\Widget\Button; use Magento\CurrencySymbol\Block\Adminhtml\System\Currency; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Element\BlockInterface; use Magento\Framework\View\LayoutInterface; use PHPUnit\Framework\TestCase; +use Magento\Framework\UrlInterface; class CurrencyTest extends TestCase { + /** + * Stub currency option link url + */ + const STUB_OPTION_LINK_URL = 'https://localhost/admin/system_config/edit/section/currency#currency_options-link'; + /** * Object manager helper * @@ -70,12 +77,25 @@ public function testPrepareLayout() ] ); + $contextMock = $this->createMock(Context::class); + $urlBuilderMock = $this->createMock(UrlInterface::class); + + $contextMock->expects($this->once())->method('getUrlBuilder')->willReturn($urlBuilderMock); + + $urlBuilderMock->expects($this->once())->method('getUrl')->with( + 'adminhtml/system_config/edit', + [ + 'section' => 'currency', + '_fragment' => 'currency_options-link' + ] + )->willReturn(self::STUB_OPTION_LINK_URL); + $childBlockMock->expects($this->at(1)) ->method('addChild') ->with( 'options_button', Button::class, - ['label' => __('Options'), 'onclick' => 'setLocation(\'\')'] + ['label' => __('Options'), 'onclick' => 'setLocation(\''.self::STUB_OPTION_LINK_URL.'\')'] ); $childBlockMock->expects($this->at(2)) @@ -90,7 +110,8 @@ public function testPrepareLayout() $block = $this->objectManagerHelper->getObject( Currency::class, [ - 'layout' => $layoutMock + 'layout' => $layoutMock, + 'context' => $contextMock ] ); $block->setLayout($layoutMock); diff --git a/app/code/Magento/Customer/Model/Customer/Authorization.php b/app/code/Magento/Customer/Model/Customer/Authorization.php new file mode 100644 index 0000000000000..5df3dbc51b732 --- /dev/null +++ b/app/code/Magento/Customer/Model/Customer/Authorization.php @@ -0,0 +1,82 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Customer; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\AuthorizationInterface; +use Magento\Integration\Api\AuthorizationServiceInterface as AuthorizationService; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Checks if customer is logged in and authorized in the current store + */ +class Authorization implements AuthorizationInterface +{ + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var CustomerFactory + */ + private $customerFactory; + + /** + * @var CustomerResource + */ + private $customerResource; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * Authorization constructor. + * + * @param UserContextInterface $userContext + * @param CustomerFactory $customerFactory + * @param CustomerResource $customerResource + * @param StoreManagerInterface $storeManager + */ + public function __construct( + UserContextInterface $userContext, + CustomerFactory $customerFactory, + CustomerResource $customerResource, + StoreManagerInterface $storeManager + ) { + $this->userContext = $userContext; + $this->customerFactory = $customerFactory; + $this->customerResource = $customerResource; + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function isAllowed($resource, $privilege = null) + { + if ($resource === AuthorizationService::PERMISSION_SELF + && $this->userContext->getUserId() + && $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER + ) { + $customer = $this->customerFactory->create(); + $this->customerResource->load($customer, $this->userContext->getUserId()); + $currentStoreId = $this->storeManager->getStore()->getId(); + $sharedStoreIds = $customer->getSharedStoreIds(); + + return in_array($currentStoreId, $sharedStoreIds); + } + + return false; + } +} diff --git a/app/code/Magento/Customer/Model/Customer/AuthorizationComposite.php b/app/code/Magento/Customer/Model/Customer/AuthorizationComposite.php new file mode 100644 index 0000000000000..716719470796e --- /dev/null +++ b/app/code/Magento/Customer/Model/Customer/AuthorizationComposite.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\Model\Customer; + +use Magento\Framework\AuthorizationInterface; + +/** + * Class to invalidate user credentials + */ +class AuthorizationComposite implements AuthorizationInterface +{ + /** + * @var AuthorizationInterface[] + */ + private $authorizationChecks; + + /** + * AuthorizationComposite constructor. + * + * @param AuthorizationInterface[] $authorizationChecks + */ + public function __construct( + array $authorizationChecks + ) { + $this->authorizationChecks = $authorizationChecks; + } + + /** + * @inheritdoc + */ + public function isAllowed($resource, $privilege = null) + { + $result = false; + + foreach ($this->authorizationChecks as $authorizationCheck) { + $result = $authorizationCheck->isAllowed($resource, $privilege); + if (!$result) { + break; + } + } + + return $result; + } +} diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php index 09af4e296bd92..211a71d827f7e 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php @@ -73,7 +73,7 @@ public function execute(string $resetPasswordToken):CustomerInterface } if ($found->getTotalCount() === 0) { //Customer with such token not found. - new NoSuchEntityException( + throw new NoSuchEntityException( new Phrase( 'No such entity with rp_token = %value', [ diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php index b877b2cca67a5..271d8f795d6f6 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php @@ -6,11 +6,9 @@ namespace Magento\Customer\Model\Plugin; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Customer\Model\CustomerFactory; -use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; -use Magento\Integration\Api\AuthorizationServiceInterface as AuthorizationService; -use Magento\Store\Model\StoreManagerInterface; +use Closure; +use Magento\Customer\Model\Customer\AuthorizationComposite; +use Magento\Framework\Authorization; /** * Plugin around \Magento\Framework\Authorization::isAllowed @@ -20,74 +18,38 @@ class CustomerAuthorization { /** - * @var UserContextInterface + * @var AuthorizationComposite */ - private $userContext; - - /** - * @var CustomerFactory - */ - private $customerFactory; - - /** - * @var CustomerResource - */ - private $customerResource; - - /** - * @var StoreManagerInterface - */ - private $storeManager; + private $authorizationComposite; /** * Inject dependencies. - * - * @param UserContextInterface $userContext - * @param CustomerFactory $customerFactory - * @param CustomerResource $customerResource - * @param StoreManagerInterface $storeManager + * @param AuthorizationComposite $composite */ public function __construct( - UserContextInterface $userContext, - CustomerFactory $customerFactory, - CustomerResource $customerResource, - StoreManagerInterface $storeManager + AuthorizationComposite $composite ) { - $this->userContext = $userContext; - $this->customerFactory = $customerFactory; - $this->customerResource = $customerResource; - $this->storeManager = $storeManager; + $this->authorizationComposite = $composite; } /** - * Check if resource for which access is needed has self permissions defined in webapi config. + * Verify if to allow customer users to access resources with self permission * - * @param \Magento\Framework\Authorization $subject - * @param callable $proceed - * @param string $resource - * @param string $privilege - * - * @return bool true If resource permission is self, to allow - * customer access without further checks in parent method * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @param Authorization $subject + * @param Closure $proceed + * @param string $resource + * @param mixed $privilege + * @return bool */ public function aroundIsAllowed( - \Magento\Framework\Authorization $subject, - \Closure $proceed, - $resource, + Authorization $subject, + Closure $proceed, + string $resource, $privilege = null ) { - if ($resource == AuthorizationService::PERMISSION_SELF - && $this->userContext->getUserId() - && $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER - ) { - $customer = $this->customerFactory->create(); - $this->customerResource->load($customer, $this->userContext->getUserId()); - $currentStoreId = $this->storeManager->getStore()->getId(); - $sharedStoreIds = $customer->getSharedStoreIds(); - if (in_array($currentStoreId, $sharedStoreIds)) { - return true; - } + if ($this->authorizationComposite->isAllowed($resource, $privilege)) { + return true; } return $proceed($resource, $privilege); diff --git a/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php b/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php index 41311abee5da8..fd5004ae0548f 100644 --- a/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php +++ b/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Observer; @@ -17,6 +18,7 @@ use Magento\Framework\App\State as AppState; use Magento\Framework\DataObject; use Magento\Framework\Escaper; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\Message\ManagerInterface; use Magento\Framework\Registry; @@ -25,6 +27,7 @@ /** * Customer Observer Model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AfterAddressSaveObserver implements ObserverInterface { @@ -114,11 +117,11 @@ public function __construct( /** * Address after save event handler * - * @param \Magento\Framework\Event\Observer $observer + * @param Observer $observer * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { /** @var $customerAddress Address */ $customerAddress = $observer->getCustomerAddress(); @@ -280,7 +283,7 @@ protected function addInvalidMessage($customerAddress) $message[] = (string)__('You will be charged tax.'); } - $this->messageManager->addError(implode(' ', $message)); + $this->messageManager->addErrorMessage(implode(' ', $message)); return $this; } @@ -307,7 +310,7 @@ protected function addErrorMessage($customerAddress) $email = $this->scopeConfig->getValue('trans_email/ident_support/email', ScopeInterface::SCOPE_STORE); $message[] = (string)__('If you believe this is an error, please contact us at %1', $email); - $this->messageManager->addError(implode(' ', $message)); + $this->messageManager->addErrorMessage(implode(' ', $message)); return $this; } diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminClickFirstRowEditLinkOnCustomerGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminClickFirstRowEditLinkOnCustomerGridActionGroup.xml new file mode 100644 index 0000000000000..0cfe9f80d1619 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminClickFirstRowEditLinkOnCustomerGridActionGroup.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="AdminClickFirstRowEditLinkOnCustomerGridActionGroup"> + <annotations> + <description>Click edit link for first row on the grid.</description> + </annotations> + + <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditLink"/> + <waitForPageLoad stepKey="waitForPageLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml index bbdc4de330840..cd581ed1836dd 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml @@ -14,6 +14,6 @@ </arguments> <fillField userInput="{{productName}}" selector="{{AdminCustomerWishlistSection.productName}}" stepKey="fillProductNameField"/> <click selector="{{AdminCustomerWishlistSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForGridLoading"/> + <waitForAjaxLoad time="60" stepKey="waitForLoading"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerLogoutSuccessPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerLogoutSuccessPageActionGroup.xml new file mode 100644 index 0000000000000..495f4504fcfd5 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerLogoutSuccessPageActionGroup.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="AssertStorefrontCustomerLogoutSuccessPageActionGroup"> + <annotations> + <description>Assert on the Storefront Customer Logout Success Page page.</description> + </annotations> + + <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" stepKey="seeOnSignInPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml index e176c45a1fa00..5db0b8f5581d7 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/CustomerData.xml @@ -285,6 +285,21 @@ <requiredEntity type="address">DE_Address_Berlin_Not_Default_Address</requiredEntity> <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> </entity> + <entity name="Customer_DE_UK_US" 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">DE_Address_Berlin_Not_Default_Address</requiredEntity> + <requiredEntity type="address">UK_Not_Default_Address</requiredEntity> + <requiredEntity type="address">US_Address_NY</requiredEntity> + </entity> <entity name="Retailer_Customer" type="customer"> <data key="group_id">3</data> <data key="default_billing">true</data> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml index c8e3bc10cc769..9f6d8d645e5f4 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml @@ -50,8 +50,7 @@ <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> <!--Assert Customer Form --> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton1"/> <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> <see selector="{{AdminCustomerAccountInformationSection.groupIdValue}}" userInput="Retailer" stepKey="seeCustomerGroup1"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml index 5f496e2c5fba3..782c1599bf489 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryPolandTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton"/> <!-- Add the Address --> <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="selectAddress"/> @@ -67,8 +66,7 @@ <see userInput="{{PolandAddress.telephone}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPhoneNumber"/> <!--Assert Customer Form --> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton1"/> <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="$$createCustomer.firstname$$" stepKey="seeCustomerFirstName"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml index da2eed2006434..304d545fb4c93 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCountryUSATest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterTheCustomerByEmail"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton"/> <!-- Add the Address --> <click selector="{{AdminEditCustomerAddressesSection.addresses}}" stepKey="selectAddress"/> @@ -67,8 +66,7 @@ <see userInput="{{US_Address_CA.telephone}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertPhoneNumber"/> <!--Assert Customer Form --> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton1"/> <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="$$createCustomer.firstname$$" stepKey="seeCustomerFirstName"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml index 8afd1648d26e0..7cffd5f304e31 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml @@ -54,8 +54,7 @@ <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> <!--Assert Customer Form --> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton1"/> <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> <see selector="{{AdminCustomerAccountInformationSection.groupIdValue}}" userInput="$$customerGroup.code$$" stepKey="seeCustomerGroup1"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml index e9250be637534..eaa3a11edb74e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml @@ -56,8 +56,7 @@ <see userInput="Male" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertGender"/> <!--Assert Customer Form --> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton1"/> <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> <seeInField selector="{{AdminCustomerAccountInformationSection.namePrefix}}" userInput="{{CustomerEntityOne.prefix}}" stepKey="seeCustomerNamePrefix"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml index 5033f2882af42..98826b147ad81 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml @@ -49,8 +49,7 @@ <see userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerGridSection.customerGrid}}" stepKey="assertEmail"/> <!--Assert Customer Form --> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad1"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton1"/> <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> <waitForPageLoad stepKey="waitForCustomerInformationPageToLoad"/> <seeInField selector="{{AdminCustomerAccountInformationSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="seeCustomerFirstName"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml index 5440339e3a95e..683b275ca1ed6 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml @@ -45,8 +45,7 @@ <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"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickFirstRowEditLink"/> <click selector="{{AdminEditCustomerInformationSection.newsLetter}}" stepKey="clickNewsLetter"/> <waitForPageLoad stepKey="waitForNewsletterTabToOpen"/> <seeCheckboxIsChecked selector="{{AdminEditCustomerNewsletterSection.subscribedStatus('1')}}" stepKey="seeAssertSubscribedToNewsletterCheckboxIsChecked"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml index 6b484e857d276..5edb9d08da46d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml @@ -42,8 +42,7 @@ <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickOnEditButton1"/> - <waitForPageLoad stepKey="waitForCustomerEditPageToLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickOnEditButton1"/> <!-- Assert Customer Title --> <click selector="{{AdminCustomerAccountInformationSection.accountInformationButton}}" stepKey="clickOnAccountInformation"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml index a8391458a1a50..87111ec6fba1a 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCustomerSubscribeNewsletterPerWebsiteTest.xml @@ -53,8 +53,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCustomerGrid"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickToEditCustomerPage"/> - <waitForPageLoad stepKey="waitForOpenCustomerPage"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickToEditCustomerPage"/> <grabFromCurrentUrl regex="~(\d+)/~" stepKey="grabCustomerId"/> <!-- Assert that created customer is subscribed to newsletter on the new Store View --> <actionGroup ref="AdminAssertCustomerIsSubscribedToNewslettersAndSelectedStoreView" stepKey="assertSubscribedToNewsletter"> diff --git a/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/GetCustomerByTokenTest.php b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/GetCustomerByTokenTest.php new file mode 100644 index 0000000000000..67dbb136297ff --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/GetCustomerByTokenTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model\ForgotPasswordToken; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerSearchResultsInterface; +use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\State\ExpiredException; +use Magento\Framework\Phrase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class GetCustomerByTokenTest extends TestCase +{ + private const RESET_PASSWORD = 'resetPassword'; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var SearchCriteria|MockObject + */ + private $searchCriteriaMock; + + /** + * @var CustomerRepositoryInterface|MockObject + */ + private $customerRepositoryMock; + + /** + * @var CustomerSearchResultsInterface|MockObject + */ + private $searchResultMock; + + /** + * @var CustomerInterface|MockObject + */ + private $customerMock; + + /** + * @var GetCustomerByToken; + */ + private $model; + + protected function setUp(): void + { + $this->searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + $this->searchCriteriaMock = $this->createMock(SearchCriteria::class); + $this->searchResultMock = $this->createMock(CustomerSearchResultsInterface::class); + $this->customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class); + $this->customerMock = $this->getMockForAbstractClass(CustomerInterface::class); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + GetCustomerByToken::class, + [ + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'customerRepository' => $this->customerRepositoryMock + ] + ); + + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($this->searchCriteriaMock); + $this->customerRepositoryMock->expects($this->once()) + ->method('getList') + ->with($this->searchCriteriaMock) + ->willReturn($this->searchResultMock); + } + + public function testExecuteReturnWhenOneItemAvailable(): void + { + $totalCount = 1; + $this->searchResultMock->method('getTotalCount')->willReturn($totalCount); + $this->searchResultMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->customerMock]); + + $this->assertInstanceOf( + CustomerInterface::class, + $this->model->execute(self::RESET_PASSWORD) + ); + } + + public function testExecuteWithNoSuchEntityException(): void + { + $totalCount = 0; + $this->searchResultMock->method('getTotalCount')->willReturn($totalCount); + $this->expectExceptionObject(new NoSuchEntityException( + new Phrase( + 'No such entity with rp_token = %value', + ['value' => self::RESET_PASSWORD] + ) + )); + + $this->model->execute(self::RESET_PASSWORD); + } + + public function testExecuteWithExpireException(): void + { + $totalCount = 2; + $this->searchResultMock->method('getTotalCount')->willReturn($totalCount); + + $this->expectExceptionObject(new ExpiredException( + new Phrase( + 'Reset password token expired.' + ) + )); + + $this->model->execute(self::RESET_PASSWORD); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php index 7232317af8ade..f72cbbc281e90 100644 --- a/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php +++ b/app/code/Magento/Customer/Test/Unit/Observer/AfterAddressSaveObserverTest.php @@ -80,29 +80,23 @@ class AfterAddressSaveObserverTest extends TestCase protected $appState; /** - * @var Customer|MockObject + * @var Session|MockObject */ - protected $customerMock; + protected $customerSessionMock; /** - * @var Session|MockObject + * @var GroupInterface|MockObject */ - protected $customerSessionMock; + protected $group; protected function setUp(): void { - $this->vat = $this->getMockBuilder(Vat::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->helperAddress = $this->getMockBuilder(\Magento\Customer\Helper\Address::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->registry = $this->getMockBuilder(Registry::class) - ->disableOriginalConstructor() - ->getMock(); - + $this->vat = $this->createMock(Vat::class); + $this->helperAddress = $this->createMock(HelperAddress::class); + $this->registry = $this->createMock(Registry::class); + $this->escaper = $this->createMock(Escaper::class); + $this->appState = $this->createMock(AppState::class); + $this->customerSessionMock = $this->createMock(Session::class); $this->group = $this->getMockBuilder(GroupInterface::class) ->setMethods(['getId']) ->getMockForAbstractClass(); @@ -114,22 +108,9 @@ protected function setUp(): void $this->scopeConfig = $this->getMockBuilder(ScopeConfigInterface::class) ->getMockForAbstractClass(); - $this->messageManager = $this->getMockBuilder(ManagerInterface::class) ->getMockForAbstractClass(); - $this->escaper = $this->getMockBuilder(Escaper::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->appState = $this->getMockBuilder(\Magento\Framework\App\State::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->customerSessionMock = $this->getMockBuilder(Session::class) - ->disableOriginalConstructor() - ->getMock(); - $this->model = new AfterAddressSaveObserver( $this->vat, $this->helperAddress, @@ -595,7 +576,7 @@ public function testAfterAddressSaveNewGroup( ->with($vatId) ->willReturn($vatId); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($resultInvalidMessage) ->willReturnSelf(); } @@ -605,7 +586,7 @@ public function testAfterAddressSaveNewGroup( ->with('trans_email/ident_support/email', ScopeInterface::SCOPE_STORE) ->willReturn('admin@example.com'); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($resultErrorMessage) ->willReturnSelf(); } diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index a349d07a5e222..d07d1a61c3d62 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -22,4 +22,13 @@ <type name="Magento\Customer\Api\CustomerRepositoryInterface"> <plugin name="updateCustomerByIdFromRequest" type="Magento\Customer\Model\Plugin\UpdateCustomer" /> </type> + <type name="Magento\Customer\Model\Customer\AuthorizationComposite"> + <arguments> + <argument name="authorizationChecks" xsi:type="array"> + <item name="rest_customer_authorization" xsi:type="object"> + Magento\Customer\Model\Customer\Authorization + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Customer/etc/webapi_soap/di.xml b/app/code/Magento/Customer/etc/webapi_soap/di.xml index 646ba98b4c5d8..c23de8ef3f7e1 100644 --- a/app/code/Magento/Customer/etc/webapi_soap/di.xml +++ b/app/code/Magento/Customer/etc/webapi_soap/di.xml @@ -9,4 +9,13 @@ <type name="Magento\Framework\Authorization"> <plugin name="customerAuthorization" type="Magento\Customer\Model\Plugin\CustomerAuthorization" /> </type> + <type name="Magento\Customer\Model\Customer\AuthorizationComposite"> + <arguments> + <argument name="authorizationChecks" xsi:type="array"> + <item name="soap_customer_authorization" xsi:type="object"> + Magento\Customer\Model\Customer\Authorization + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js index 5c9bf431bac1d..5321dfecba182 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js +++ b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js @@ -261,6 +261,9 @@ define([ } }); + //remove expired section names of previously installed/enable modules + expiredSectionNames = _.intersection(expiredSectionNames, sectionConfig.getSectionNames()); + return _.uniq(expiredSectionNames); }, diff --git a/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php b/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php index 5800f7162b7f0..15f9031c2c769 100644 --- a/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php +++ b/app/code/Magento/Developer/Console/Command/GeneratePatchCommand.php @@ -133,6 +133,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $type = $input->getOption(self::INPUT_KEY_PATCH_TYPE); $modulePath = $this->componentRegistrar->getPath(ComponentRegistrar::MODULE, $moduleName); + if (null === $modulePath) { + throw new \InvalidArgumentException(sprintf('Cannot find a registered module with name "%s"', $moduleName)); + } $preparedModuleName = str_replace('_', '\\', $moduleName); $preparedType = ucfirst($type); $patchInterface = sprintf('%sPatchInterface', $preparedType); diff --git a/app/code/Magento/Developer/Test/Unit/Console/Command/GeneratePatchCommandTest.php b/app/code/Magento/Developer/Test/Unit/Console/Command/GeneratePatchCommandTest.php new file mode 100644 index 0000000000000..6fa1ca8a4674a --- /dev/null +++ b/app/code/Magento/Developer/Test/Unit/Console/Command/GeneratePatchCommandTest.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Developer\Test\Unit\Console\Command; + +use Magento\Developer\Console\Command\GeneratePatchCommand; +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\Framework\Filesystem\Directory\ReadFactory; +use Magento\Framework\Filesystem\Directory\Write; +use Magento\Framework\Filesystem\Directory\WriteFactory; +use Magento\Framework\Filesystem\DirectoryList; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Tester\CommandTester; + +class GeneratePatchCommandTest extends TestCase +{ + /** + * @var ComponentRegistrar|MockObject + */ + private $componentRegistrarMock; + + /** + * @var DirectoryList|MockObject + */ + private $directoryListMock; + + /** + * @var ReadFactory|MockObject + */ + private $readFactoryMock; + + /** + * @var WriteFactory|MockObject + */ + private $writeFactoryMock; + + /** + * @var GeneratePatchCommand|MockObject + */ + private $command; + + protected function setUp(): void + { + $this->componentRegistrarMock = $this->createMock(ComponentRegistrar::class); + $this->directoryListMock = $this->createMock(DirectoryList::class); + $this->readFactoryMock = $this->createMock(ReadFactory::class); + $this->writeFactoryMock = $this->createMock(WriteFactory::class); + + $this->command = new GeneratePatchCommand( + $this->componentRegistrarMock, + $this->directoryListMock, + $this->readFactoryMock, + $this->writeFactoryMock + ); + } + + public function testExecute() + { + $this->componentRegistrarMock->expects($this->once()) + ->method('getPath') + ->with('module', 'Vendor_Module') + ->willReturn('/long/path/to/Vendor/Module'); + + $read = $this->createMock(Read::class); + $read->expects($this->at(0)) + ->method('readFile') + ->with('patch_template.php.dist') + ->willReturn('something'); + $this->readFactoryMock->method('create')->willReturn($read); + + $write = $this->createMock(Write::class); + $write->expects($this->once())->method('writeFile'); + $this->writeFactoryMock->method('create')->willReturn($write); + + $this->directoryListMock->expects($this->once())->method('getRoot')->willReturn('/some/path'); + + $commandTester = new CommandTester($this->command); + $commandTester->execute( + [ + GeneratePatchCommand::MODULE_NAME => 'Vendor_Module', + GeneratePatchCommand::INPUT_KEY_PATCH_NAME => 'SomePatch' + ] + ); + $this->assertStringContainsString('successfully generated', $commandTester->getDisplay()); + } + + public function testWrongParameter() + { + $this->expectExceptionMessage('Not enough arguments'); + $this->expectException(\RuntimeException::class); + + $commandTester = new CommandTester($this->command); + $commandTester->execute([]); + } + + public function testBadModule() + { + $this->componentRegistrarMock->expects($this->once()) + ->method('getPath') + ->with('module', 'Fake_Module') + ->willReturn(null); + + $this->expectExceptionMessage('Cannot find a registered module with name "Fake_Module"'); + $this->expectException(\InvalidArgumentException::class); + + $commandTester = new CommandTester($this->command); + $commandTester->execute( + [ + GeneratePatchCommand::MODULE_NAME => 'Fake_Module', + GeneratePatchCommand::INPUT_KEY_PATCH_NAME => 'SomePatch' + ] + ); + } +} diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index d9b89b23d3d69..204094571ba3b 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -676,6 +676,7 @@ public function getDhlProducts($doc) 'H' => __('Economy select'), 'J' => __('Jumbo box'), 'M' => __('Express 10:30'), + 'N' => __('Domestic express'), 'V' => __('Europack'), 'Y' => __('Express 12:00'), ]; @@ -1767,9 +1768,8 @@ protected function _shipmentDetails($xml, $rawRequest, $originRegion = '') */ $nodeShipmentDetails->addChild('DoorTo', 'DD'); $nodeShipmentDetails->addChild('DimensionUnit', substr($this->_getDimensionUnit(), 0, 1)); - if ($package['params']['container'] == self::DHL_CONTENT_TYPE_NON_DOC) { - $packageType = 'CP'; - } + $contentType = isset($package['params']['container']) ? $package['params']['container'] : ''; + $packageType = $contentType === self::DHL_CONTENT_TYPE_NON_DOC ? 'CP' : ''; $nodeShipmentDetails->addChild('PackageType', $packageType); if ($this->isDutiable($rawRequest->getOrigCountryId(), $rawRequest->getDestCountryId())) { $nodeShipmentDetails->addChild('IsDutiable', 'Y'); diff --git a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php index bc0321884aa0f..489157b442c8c 100644 --- a/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php +++ b/app/code/Magento/Dhl/Test/Unit/Model/CarrierTest.php @@ -333,6 +333,7 @@ public function dhlProductsDataProvider(): array 'H' => 'Economy select', 'J' => 'Jumbo box', 'M' => 'Express 10:30', + 'N' => 'Domestic express', 'V' => 'Europack', 'Y' => 'Express 12:00', ], diff --git a/app/code/Magento/Dhl/etc/config.xml b/app/code/Magento/Dhl/etc/config.xml index 3408447e70650..deb162c07ba25 100644 --- a/app/code/Magento/Dhl/etc/config.xml +++ b/app/code/Magento/Dhl/etc/config.xml @@ -21,7 +21,7 @@ <active>0</active> <title>DHL 0 - 1,3,4,8,P,Q,E,F,H,J,M,V,Y + 1,3,4,8,P,Q,E,F,H,J,M,N,V,Y 2,5,6,7,9,B,C,D,U,K,L,G,W,I,N,O,R,S,T,X G https://xmlpi-ea.dhl.com/XMLShippingServlet diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Link.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Link.php index 8d5f64e02be47..1b53afc520731 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Link.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Link.php @@ -4,27 +4,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; +use Magento\Catalog\Controller\Adminhtml\Product\Edit as ProductEdit; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Downloadable\Helper\File; +use Magento\Downloadable\Model\Link as ModelLink; use Magento\Framework\App\Response\Http as HttpResponse; -class Link extends \Magento\Catalog\Controller\Adminhtml\Product\Edit +class Link extends ProductEdit { /** - * @return \Magento\Downloadable\Model\Link + * Create link + * + * @return ModelLink */ protected function _createLink() { - return $this->_objectManager->create(\Magento\Downloadable\Model\Link::class); + return $this->_objectManager->create(ModelLink::class); } /** - * @return \Magento\Downloadable\Model\Link + * Get link + * + * @return ModelLink */ protected function _getLink() { - return $this->_objectManager->get(\Magento\Downloadable\Model\Link::class); + return $this->_objectManager->get(ModelLink::class); } /** @@ -34,10 +43,10 @@ protected function _getLink() * @param string $resourceType * @return void */ - protected function _processDownload($resource, $resourceType) + protected function _processDownload(string $resource, string $resourceType) { - /* @var $helper \Magento\Downloadable\Helper\Download */ - $helper = $this->_objectManager->get(\Magento\Downloadable\Helper\Download::class); + /* @var $helper DownloadHelper */ + $helper = $this->_objectManager->get(DownloadHelper::class); $helper->setResource($resource, $resourceType); $fileName = $helper->getFilename(); @@ -77,7 +86,7 @@ protected function _processDownload($resource, $resourceType) //Rendering $response->clearBody(); $response->sendHeaders(); - + $helper->output(); } @@ -90,7 +99,7 @@ public function execute() { $linkId = $this->getRequest()->getParam('id', 0); $type = $this->getRequest()->getParam('type', 0); - /** @var \Magento\Downloadable\Model\Link $link */ + /** @var ModelLink $link */ $link = $this->_createLink()->load($linkId); if ($link->getId()) { $resource = ''; @@ -101,7 +110,7 @@ public function execute() $resourceType = DownloadHelper::LINK_TYPE_URL; } elseif ($link->getLinkType() == DownloadHelper::LINK_TYPE_FILE) { $resource = $this->_objectManager->get( - \Magento\Downloadable\Helper\File::class + File::class )->getFilePath( $this->_getLink()->getBasePath(), $link->getLinkFile() @@ -114,7 +123,7 @@ public function execute() $resourceType = DownloadHelper::LINK_TYPE_URL; } elseif ($link->getSampleType() == DownloadHelper::LINK_TYPE_FILE) { $resource = $this->_objectManager->get( - \Magento\Downloadable\Helper\File::class + File::class )->getFilePath( $this->_getLink()->getBaseSamplePath(), $link->getSampleFile() @@ -125,7 +134,7 @@ public function execute() try { $this->_processDownload($resource, $resourceType); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError(__('Something went wrong while getting the requested content.')); + $this->messageManager->addErrorMessage(__('Something went wrong while getting the requested content.')); } } } diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Sample.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Sample.php index 2e115e1ce18d3..84bd21904ea18 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Sample.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Sample.php @@ -4,26 +4,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Downloadable\Model\Sample as ModelSample; -class Sample extends \Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit\Link +class Sample extends Link { /** - * @return \Magento\Downloadable\Model\Sample + * Create link + * + * @return ModelSample */ protected function _createLink() { - return $this->_objectManager->create(\Magento\Downloadable\Model\Sample::class); + return $this->_objectManager->create(ModelSample::class); } /** - * @return \Magento\Downloadable\Model\Sample + * Get link + * + * @return ModelSample */ protected function _getLink() { - return $this->_objectManager->get(\Magento\Downloadable\Model\Sample::class); + return $this->_objectManager->get(ModelSample::class); } /** @@ -34,7 +41,7 @@ protected function _getLink() public function execute() { $sampleId = $this->getRequest()->getParam('id', 0); - /** @var \Magento\Downloadable\Model\Sample $sample */ + /** @var ModelSample $sample */ $sample = $this->_createLink()->load($sampleId); if ($sample->getId()) { $resource = ''; @@ -54,7 +61,7 @@ public function execute() try { $this->_processDownload($resource, $resourceType); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError(__('Something went wrong while getting the requested content.')); + $this->messageManager->addErrorMessage(__('Something went wrong while getting the requested content.')); } } } diff --git a/app/code/Magento/Downloadable/Controller/Download/Link.php b/app/code/Magento/Downloadable/Controller/Download/Link.php index 4766f1699afb6..2b131806fa022 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Link.php +++ b/app/code/Magento/Downloadable/Controller/Download/Link.php @@ -125,7 +125,7 @@ public function execute() // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } catch (\Exception $e) { - $this->messageManager->addError(__('Something went wrong while getting the requested content.')); + $this->messageManager->addErrorMessage(__('Something went wrong while getting the requested content.')); } } elseif ($status == PurchasedLink::LINK_STATUS_EXPIRED) { $this->messageManager->addNotice(__('The link has expired.')); @@ -133,7 +133,7 @@ public function execute() ) { $this->messageManager->addNotice(__('The link is not available.')); } else { - $this->messageManager->addError(__('Something went wrong while getting the requested content.')); + $this->messageManager->addErrorMessage(__('Something went wrong while getting the requested content.')); } return $this->_redirect('*/customer/products'); } diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php index c449f8f54872f..1be97435fff84 100644 --- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php +++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php @@ -69,7 +69,7 @@ public function execute() // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage exit(0); } catch (\Exception $e) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('Sorry, there was an error getting requested content. Please contact the store owner.') ); } diff --git a/app/code/Magento/Downloadable/Observer/SetLinkStatusObserver.php b/app/code/Magento/Downloadable/Observer/SetLinkStatusObserver.php index 971feafb857a9..2a07a3a49639f 100644 --- a/app/code/Magento/Downloadable/Observer/SetLinkStatusObserver.php +++ b/app/code/Magento/Downloadable/Observer/SetLinkStatusObserver.php @@ -61,6 +61,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) 'payment_pending' => \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PENDING_PAYMENT, 'payment_review' => \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PAYMENT_REVIEW, ]; + $expiredOrderItemIds = []; $downloadableItemsStatuses = []; $orderItemStatusToEnable = $this->_scopeConfig->getValue( @@ -114,6 +115,10 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (in_array($item->getStatusId(), $availableStatuses)) { $downloadableItemsStatuses[$item->getId()] = $linkStatuses['avail']; } + + if ($item->getQtyOrdered() - $item->getQtyRefunded() == 0) { + $expiredOrderItemIds[] = $item->getId(); + } } } } @@ -141,10 +146,22 @@ public function execute(\Magento\Framework\Event\Observer $observer) } } + if ($expiredOrderItemIds) { + $linkPurchased = $this->_createItemsCollection()->addFieldToFilter( + 'order_item_id', + ['in' => $expiredOrderItemIds] + ); + foreach ($linkPurchased as $link) { + $link->setStatus(\Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_EXPIRED)->save(); + } + } + return $this; } /** + * Returns purchased item collection + * * @return \Magento\Downloadable\Model\ResourceModel\Link\Purchased\Item\Collection */ protected function _createItemsCollection() diff --git a/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontNotAssertDownloadableProductLinkInCustomerAccountActionGroup.xml b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontNotAssertDownloadableProductLinkInCustomerAccountActionGroup.xml new file mode 100644 index 0000000000000..ae288c7033e17 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/ActionGroup/StorefrontNotAssertDownloadableProductLinkInCustomerAccountActionGroup.xml @@ -0,0 +1,26 @@ + + + + + + + Goes to the Storefront Customer Dashboard page. Clicks on 'My Downloadable Products'. Validates that the provided Downloadable Product is present and Downloadable link not exist. + + + + + + + + + + + + + diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontCustomerDownloadableProductsSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontCustomerDownloadableProductsSection.xml index d45a774077ba0..5d340e6c91060 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontCustomerDownloadableProductsSection.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontCustomerDownloadableProductsSection.xml @@ -10,5 +10,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
+
diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml new file mode 100644 index 0000000000000..d82cc25b0eccf --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontAccountDownloadableProductLinkAfterPartialRefundTest.xml @@ -0,0 +1,109 @@ + + + + + + + + + <description value="Verify that Downloadable product is not available in My Download Products tab after it has been partially refunded."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-35198"/> + <group value="Downloadable"/> + </annotations> + + <before> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiDownloadableProduct" stepKey="createDownloadableProduct"/> + <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> + <requiredEntity createDataKey="createDownloadableProduct"/> + </createData> + + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signIn"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createDownloadableProduct" stepKey="deleteDownloadableProduct"/> + + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </after> + + <actionGroup ref="StorefrontAddSimpleProductToShoppingCartActionGroup" stepKey="addSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <amOnPage url="{{StorefrontProductPage.url($$createDownloadableProduct.custom_attributes[url_key]$$)}}" stepKey="OpenStoreFrontProductPage"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <actionGroup ref="StorefrontAddToCartCustomOptionsProductPageActionGroup" stepKey="addToTheCart"> + <argument name="productName" value="$$createDownloadableProduct.name$$"/> + </actionGroup> + + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForPageLoad stepKey="waitForProceedToCheckout"/> + <waitForElementVisible selector="{{CheckoutShippingSection.shipHereButton(UK_Not_Default_Address.street[0])}}" stepKey="waitForShipHereVisible"/> + <click selector="{{CheckoutShippingSection.shipHereButton(UK_Not_Default_Address.street[0])}}" stepKey="clickShipHere"/> + <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForShipmentPageLoad"/> + <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> + <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchOrder"> + <argument name="keyword" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + + <actionGroup ref="AdminCreateInvoiceActionGroup" stepKey="createCreditMemo"/> + + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrder"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + + <actionGroup ref="AdminOpenAndFillCreditMemoRefundActionGroup" stepKey="fillCreditMemoRefund"> + <argument name="itemQtyToRefund" value="0"/> + <argument name="rowNumber" value="1"/> + </actionGroup> + + <click selector="{{AdminCreditMemoTotalSection.submitRefundOffline}}" stepKey="clickRefundOffline"/> + <waitForPageLoad stepKey="waitForResultPage"/> + + <actionGroup ref="StorefrontNotAssertDownloadableProductLinkInCustomerAccountActionGroup" stepKey="dontSeeStorefrontMyAccountDownloadableProductsLink"> + <argument name="product" value="$$createDownloadableProduct$$"/> + </actionGroup> + + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Downloadable/Product/Edit/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Downloadable/Product/Edit/SampleTest.php index 193b001f305b2..31cba7b601eec 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Downloadable/Product/Edit/SampleTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Controller/Adminhtml/Downloadable/Product/Edit/SampleTest.php @@ -184,6 +184,8 @@ public function testExecuteUrl() ->willReturn('1'); $this->sampleModel->expects($this->any())->method('getSampleType') ->willReturn('url'); + $this->sampleModel->expects($this->once())->method('getSampleUrl') + ->willReturn('http://example.com/simple.jpg'); $this->objectManager->expects($this->once())->method('create') ->willReturn($this->sampleModel); diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkTest.php index b7483f3658d69..f9e464a3948f1 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkTest.php @@ -327,7 +327,7 @@ public function testExceptionInUpdateLinkStatus($mimeType, $disposition) $this->linkPurchasedItem->expects($this->any())->method('setStatus')->with('expired')->willReturnSelf(); $this->linkPurchasedItem->expects($this->any())->method('save')->willThrowException(new \Exception()); $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('Something went wrong while getting the requested content.') ->willReturnSelf(); $this->redirect->expects($this->once())->method('redirect')->with($this->response, '*/customer/products', []); @@ -494,7 +494,7 @@ public function linkNotAvailableDataProvider() ['addNotice', 'expired', 'The link has expired.'], ['addNotice', 'pending', 'The link is not available.'], ['addNotice', 'payment_review', 'The link is not available.'], - ['addError', 'wrong_status', 'Something went wrong while getting the requested content.'] + ['addErrorMessage', 'wrong_status', 'Something went wrong while getting the requested content.'] ]; } diff --git a/app/code/Magento/Downloadable/Test/Unit/Observer/SetLinkStatusObserverTest.php b/app/code/Magento/Downloadable/Test/Unit/Observer/SetLinkStatusObserverTest.php index 46a3ef6717582..b5be0309bb5be 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Observer/SetLinkStatusObserverTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Observer/SetLinkStatusObserverTest.php @@ -189,7 +189,7 @@ public function testSetLinkStatusPending($orderState, array $orderStateMapping) ] ); - $this->itemsFactory->expects($this->once()) + $this->itemsFactory->expects($this->any()) ->method('create') ->willReturn( $this->createLinkItemCollection( @@ -243,7 +243,7 @@ public function testSetLinkStatusClosed() ] ); - $this->itemsFactory->expects($this->once()) + $this->itemsFactory->expects($this->any()) ->method('create') ->willReturn( $this->createLinkItemCollection( @@ -308,7 +308,7 @@ public function testSetLinkStatusInvoiced() ] ); - $this->itemsFactory->expects($this->once()) + $this->itemsFactory->expects($this->any()) ->method('create') ->willReturn( $this->createLinkItemCollection( @@ -344,6 +344,137 @@ public function testSetLinkStatusEmptyOrder() $this->assertInstanceOf(SetLinkStatusObserver::class, $result); } + public function testSetLinkStatusExpired() + { + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->with( + \Magento\Downloadable\Model\Link\Purchased\Item::XML_PATH_ORDER_ITEM_STATUS, + ScopeInterface::SCOPE_STORE, + 1 + ) + ->willReturn(Item::STATUS_PENDING); + + $this->observerMock->expects($this->once()) + ->method('getEvent') + ->willReturn($this->eventMock); + + $this->eventMock->expects($this->once()) + ->method('getOrder') + ->willReturn($this->orderMock); + + $this->orderMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->orderMock->expects($this->once()) + ->method('getStoreId') + ->willReturn(1); + + $this->orderMock->expects($this->atLeastOnce()) + ->method('getState') + ->willReturn(Order::STATE_PROCESSING); + + $this->orderMock->expects($this->any()) + ->method('getAllItems') + ->willReturn( + [ + $this->createRefundOrderItem(2, 2, 2), + $this->createRefundOrderItem(3, 2, 1), + $this->createRefundOrderItem(4, 3, 3), + ] + ); + + $this->itemsFactory->expects($this->any()) + ->method('create') + ->willReturn( + $this->createLinkItemToExpireCollection( + [2, 4], + [ + $this->createLinkItem( + 'available', + 2, + true, + \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_EXPIRED + ), + $this->createLinkItem( + 'pending_payment', + 4, + true, + \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_EXPIRED + ), + ] + ) + ); + + $result = $this->setLinkStatusObserver->execute($this->observerMock); + $this->assertInstanceOf(SetLinkStatusObserver::class, $result); + } + + /** + * @param $id + * @param int $qtyOrdered + * @param int $qtyRefunded + * @param string $productType + * @param string $realProductType + * @return \Magento\Sales\Model\Order\Item|MockObject + */ + private function createRefundOrderItem( + $id, + $qtyOrdered, + $qtyRefunded, + $productType = DownloadableProductType::TYPE_DOWNLOADABLE, + $realProductType = DownloadableProductType::TYPE_DOWNLOADABLE + ) { + $item = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->setMethods([ + 'getId', + 'getQtyOrdered', + 'getQtyRefunded', + 'getProductType', + 'getRealProductType' + ])->getMock(); + $item->expects($this->any()) + ->method('getId') + ->willReturn($id); + $item->expects($this->any()) + ->method('getQtyOrdered') + ->willReturn($qtyOrdered); + $item->expects($this->any()) + ->method('getQtyRefunded') + ->willReturn($qtyRefunded); + $item->expects($this->any()) + ->method('getProductType') + ->willReturn($productType); + $item->expects($this->any()) + ->method('getRealProductType') + ->willReturn($realProductType); + + return $item; + } + + /** + * @param array $expectedOrderItemIds + * @param array $items + * @return LinkItemCollection|MockObject + */ + private function createLinkItemToExpireCollection(array $expectedOrderItemIds, array $items) + { + $linkItemCollection = $this->getMockBuilder( + \Magento\Downloadable\Model\ResourceModel\Link\Purchased\Item\Collection::class + ) + ->disableOriginalConstructor() + ->setMethods(['addFieldToFilter']) + ->getMock(); + $linkItemCollection->expects($this->any()) + ->method('addFieldToFilter') + ->with('order_item_id', ['in' => $expectedOrderItemIds]) + ->willReturn($items); + + return $linkItemCollection; + } + /** * @param $id * @param int $statusId @@ -359,7 +490,7 @@ private function createOrderItem( ) { $item = $this->getMockBuilder(Item::class) ->disableOriginalConstructor() - ->setMethods(['getId', 'getProductType', 'getRealProductType', 'getStatusId']) + ->setMethods(['getId', 'getProductType', 'getRealProductType', 'getStatusId', 'getQtyOrdered']) ->getMock(); $item->expects($this->any()) ->method('getId') @@ -373,6 +504,9 @@ private function createOrderItem( $item->expects($this->any()) ->method('getStatusId') ->willReturn($statusId); + $item->expects($this->any()) + ->method('getQtyOrdered') + ->willReturn(1); return $item; } @@ -390,7 +524,7 @@ private function createLinkItemCollection(array $expectedOrderItemIds, array $it ->disableOriginalConstructor() ->setMethods(['addFieldToFilter']) ->getMock(); - $linkItemCollection->expects($this->once()) + $linkItemCollection->expects($this->any()) ->method('addFieldToFilter') ->with('order_item_id', ['in' => $expectedOrderItemIds]) ->willReturn($items); @@ -415,11 +549,11 @@ private function createLinkItem($status, $orderItemId, $isSaved = false, $expect ->method('getStatus') ->willReturn($status); if ($isSaved) { - $linkItem->expects($this->once()) + $linkItem->expects($this->any()) ->method('setStatus') ->with($expectedStatus) ->willReturnSelf(); - $linkItem->expects($this->once()) + $linkItem->expects($this->any()) ->method('save') ->willReturnSelf(); } diff --git a/app/code/Magento/DownloadableGraphQl/Model/Wishlist/ItemLinks.php b/app/code/Magento/DownloadableGraphQl/Model/Wishlist/ItemLinks.php new file mode 100644 index 0000000000000..68223054aa806 --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Model/Wishlist/ItemLinks.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Model\Wishlist; + +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; +use Magento\Downloadable\Helper\Catalog\Product\Configuration; +use Magento\DownloadableGraphQl\Model\ConvertLinksToArray; +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; + +/** + * Fetches the selected item downloadable links + */ +class ItemLinks implements ResolverInterface +{ + /** + * @var ConvertLinksToArray + */ + private $convertLinksToArray; + + /** + * @var Configuration + */ + private $downloadableConfiguration; + + /** + * @param ConvertLinksToArray $convertLinksToArray + * @param Configuration $downloadableConfiguration + */ + public function __construct( + ConvertLinksToArray $convertLinksToArray, + Configuration $downloadableConfiguration + ) { + $this->convertLinksToArray = $convertLinksToArray; + $this->downloadableConfiguration = $downloadableConfiguration; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$value['itemModel'] instanceof ItemInterface) { + throw new LocalizedException(__('"itemModel" should be a "%instance" instance', [ + 'instance' => ItemInterface::class + ])); + } + /** @var ItemInterface $wishlistItem */ + $itemItem = $value['itemModel']; + + $links = $this->downloadableConfiguration->getLinks($itemItem); + $links = $this->convertLinksToArray->execute($links); + + return $links; + } +} diff --git a/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml b/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml index c95667de15ac3..51a630d59ca0f 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml @@ -39,4 +39,11 @@ </argument> </arguments> </type> + <type name="Magento\WishlistGraphQl\Model\Resolver\Type\WishlistItemType"> + <arguments> + <argument name="supportedTypes" xsi:type="array"> + <item name="downloadable" xsi:type="string">DownloadableWishlistItem</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 5863e62e81b1b..ba178bb1a427e 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -64,3 +64,8 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de sample_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_file: String @deprecated(reason: "`sample_url` serves to get the downloadable sample") } + +type DownloadableWishlistItem implements WishlistItemInterface @doc(description: "A downloadable product wish list item") { + links_v2: [DownloadableProductLinks] @doc(description: "An array containing information about the selected links") @resolver(class: "\\Magento\\DownloadableGraphQl\\Model\\Wishlist\\ItemLinks") + samples: [DownloadableProductSamples] @doc(description: "An array containing information about the selected samples") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\Samples") +} diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index f2f767b4e41fa..b3737f67705d1 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -626,6 +626,8 @@ protected function _isApplicableAttribute($object, $attribute) public function walkAttributes($partMethod, array $args = [], $collectExceptionMessages = null) { $methodArr = explode('/', $partMethod); + $part = ''; + $method = ''; switch (count($methodArr)) { case 1: $part = 'attribute'; @@ -642,6 +644,7 @@ public function walkAttributes($partMethod, array $args = [], $collectExceptionM } $results = []; $suffix = $this->getAttributesCacheSuffix($args[0]); + $instance = null; foreach ($this->getAttributesByScope($suffix) as $attrCode => $attribute) { if (isset($args[0]) && is_object($args[0]) && !$this->_isApplicableAttribute($args[0], $attribute)) { continue; @@ -1337,7 +1340,9 @@ protected function _collectSaveData($newObject) if ($this->_canUpdateAttribute($attribute, $v, $origData)) { if ($this->_isAttributeValueEmpty($attribute, $v)) { $this->_aggregateDeleteData($delete, $attribute, $newObject); - } elseif (!is_numeric($v) && $v !== $origData[$k] || is_numeric($v) && $v != $origData[$k]) { + } elseif (!is_numeric($v) && $v !== $origData[$k] + || is_numeric($v) && ($v != $origData[$k] || strlen($v) !== strlen($origData[$k])) + ) { $update[$attrId] = [ 'value_id' => $attribute->getBackend()->getEntityValueId($newObject), 'value' => is_array($v) ? array_shift($v) : $v,//@TODO: MAGETWO-44182, @@ -1739,6 +1744,7 @@ public function delete($object) { try { $connection = $this->transactionManager->start($this->getConnection()); + $id = 0; if (is_numeric($object)) { $id = (int) $object; } elseif ($object instanceof \Magento\Framework\Model\AbstractModel) { diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 0e1e4f035fc14..b29d45f75c993 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -679,6 +679,9 @@ public function joinAttribute($alias, $attribute, $bind, $filter = null, $joinTy throw new LocalizedException(__('The foreign key is invalid. Verify the foreign key and try again.')); } + $entity = null; + $attrArr = []; + // try to explode combined entity/attribute if supplied if (is_string($attribute)) { $attrArr = explode('/', $attribute); @@ -1121,12 +1124,13 @@ public function _loadEntities($printQuery = false, $logQuery = false) $this->printLogQuery($printQuery, $logQuery); + /** + * Prepare select query + * @var string|\Magento\Framework\DB\Select $query + */ + $query = $this->getSelect(); + try { - /** - * Prepare select query - * @var string $query - */ - $query = $this->getSelect(); $rows = $this->_fetchAll($query); } catch (\Exception $e) { $this->printLogQuery(false, true, $query); @@ -1192,12 +1196,12 @@ public function _loadAttributes($printQuery = false, $logQuery = false) $selectGroups = $this->_resourceHelper->getLoadAttributesSelectGroups($selects); foreach ($selectGroups as $selects) { if (!empty($selects)) { + if (is_array($selects)) { + $select = implode(' UNION ALL ', $selects); + } else { + $select = $selects; + } try { - if (is_array($selects)) { - $select = implode(' UNION ALL ', $selects); - } else { - $select = $selects; - } $values = $this->getConnection()->fetchAll($select); } catch (\Exception $e) { $this->printLogQuery(true, true, $select); @@ -1238,10 +1242,12 @@ protected function _getLoadAttributesSelect($table, $attributeIds = []) ['t_d.attribute_id'] )->where( " e.entity_id IN (?)", - array_keys($this->_itemsById) + array_keys($this->_itemsById), + \Zend_Db::INT_TYPE )->where( 't_d.attribute_id IN (?)', - $attributeIds + $attributeIds, + \Zend_Db::INT_TYPE ); if ($entity->getEntityTable() == \Magento\Eav\Model\Entity::DEFAULT_ENTITY_TABLE && $entity->getTypeId()) { diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index 637d4e17e852d..29cad62bf0ca4 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -778,7 +778,8 @@ public function getValidAttributeIds($attributeIds) ['attribute_id'] )->where( 'attribute_id IN (?)', - $attributeIds + $attributeIds, + \Zend_Db::INT_TYPE ); return $connection->fetchCol($select); 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 6fce6bd2dc44e..bcd8f2bb04e69 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute/Collection.php @@ -6,7 +6,17 @@ namespace Magento\Eav\Model\ResourceModel\Entity\Attribute; +use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Type; +use Magento\Eav\Model\ResourceModel\Entity\Attribute; +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; +use Magento\Framework\Data\Collection\EntityFactoryInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +use Psr\Log\LoggerInterface; /** * EAV attribute resource collection @@ -14,8 +24,9 @@ * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection +class Collection extends AbstractCollection { /** * Add attribute set info flag @@ -25,28 +36,28 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab protected $_addSetInfoFlag = false; /** - * @var \Magento\Eav\Model\Config + * @var Config */ protected $eavConfig; /** - * @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 \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection - * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource + * @param EntityFactoryInterface $entityFactory + * @param LoggerInterface $logger + * @param FetchStrategyInterface $fetchStrategy + * @param ManagerInterface $eventManager + * @param Config $eavConfig + * @param AdapterInterface $connection + * @param AbstractDb $resource * @codeCoverageIgnore */ 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, - \Magento\Eav\Model\Config $eavConfig, - \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null + EntityFactoryInterface $entityFactory, + LoggerInterface $logger, + FetchStrategyInterface $fetchStrategy, + ManagerInterface $eventManager, + Config $eavConfig, + AdapterInterface $connection = null, + AbstractDb $resource = null ) { $this->eavConfig = $eavConfig; parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource); @@ -62,7 +73,7 @@ protected function _construct() { $this->_init( \Magento\Eav\Model\Entity\Attribute::class, - \Magento\Eav\Model\ResourceModel\Entity\Attribute::class + Attribute::class ); } @@ -94,7 +105,7 @@ protected function _getLoadDataFields() */ public function useLoadDataFields() { - $this->getSelect()->reset(\Magento\Framework\DB\Select::COLUMNS); + $this->getSelect()->reset(Select::COLUMNS); $this->getSelect()->columns($this->_getLoadDataFields()); return $this; @@ -221,7 +232,8 @@ public function setInAllAttributeSetsFilter(array $setIds) ) ->where( 'entity_attribute.attribute_set_id IN (?)', - $setIds + $setIds, + \Zend_Db::INT_TYPE ) ->group('entity_attribute.attribute_id') ->having(new \Zend_Db_Expr('COUNT(*)') . ' = ' . count($setIds)); @@ -394,7 +406,8 @@ protected function _addSetInfo() ['group_sort_order' => 'sort_order'] )->where( 'attribute_id IN (?)', - $attributeIds + $attributeIds, + \Zend_Db::INT_TYPE ); $result = $connection->fetchAll($select); @@ -481,7 +494,7 @@ public function addStoreLabel($storeId) public function getSelectCountSql() { $countSelect = parent::getSelectCountSql(); - $countSelect->reset(\Magento\Framework\DB\Select::COLUMNS); + $countSelect->reset(Select::COLUMNS); $countSelect->columns('COUNT(DISTINCT main_table.attribute_id)'); return $countSelect; } diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index 1971eeeff3147..e8c8d4c5190fe 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -152,7 +152,7 @@ public function execute($entityType, $entityData, $arguments = []) ['value' => 't.value', 'attribute_id' => 't.attribute_id'] ) ->where($metadata->getLinkField() . ' = ?', $entityData[$metadata->getLinkField()]) - ->where('attribute_id IN (?)', $attributeIds); + ->where('attribute_id IN (?)', $attributeIds, \Zend_Db::INT_TYPE); $attributeIdentifiers = []; foreach ($context as $scope) { //TODO: if (in table exists context field) diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php index 25e691972d81d..261f8d84b5baa 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php @@ -497,6 +497,12 @@ protected function prepareIndex($storeId, $indexName, $mappedIndexerId) */ private function getMappingTotalFieldsLimit(array $allAttributeTypes): int { - return count($allAttributeTypes) + self::MAPPING_TOTAL_FIELDS_BUFFER_LIMIT; + $count = count($allAttributeTypes); + foreach ($allAttributeTypes as $attributeType) { + if (isset($attributeType['fields'])) { + $count += count($attributeType['fields']); + } + } + return $count + self::MAPPING_TOTAL_FIELDS_BUFFER_LIMIT; } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php index 5abe800884ced..b070e3324ed78 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php @@ -174,7 +174,14 @@ protected function setUp(): void ->method('getAllAttributesTypes') ->willReturn( [ - 'name' => 'string', + 'name' => [ + 'type' => 'string', + 'fields' => [ + 'keyword' => [ + 'type' => "keyword", + ], + ], + ], ] ); $this->clientConfig->expects($this->any()) @@ -564,6 +571,28 @@ public function testUpdateIndexMappingWithAliasDefinition(): void $this->model->updateIndexMapping($storeId, $mappedIndexerId, $attributeCode); } + /** + * Test for get mapping total fields limit + * + * @return void + */ + public function testGetMappingTotalFieldsLimit(): void + { + $settings = [ + 'index' => [ + 'mapping' => [ + 'total_fields' => [ + 'limit' => 1002 + ] + ] + ] + ]; + $this->client->expects($this->at(1)) + ->method('createIndex') + ->with(null, ['settings' => $settings]); + $this->model->cleanIndex(1, 'product'); + } + /** * Get elasticsearch client options * diff --git a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml index a16a3aae14b49..a377cd8ae6722 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -135,7 +135,7 @@ require([ content: "{$block->escapeJs(__('Are you sure you want to strip tags?'))}", actions: { confirm: function () { - this.unconvertedText = $('template_text').value; + self.unconvertedText = $('template_text').value; $('convert_button').hide(); $('template_text').value = $('template_text').value.stripScripts().replace( new RegExp('<style[^>]*>[\\S\\s]*?</style>', 'img'), '' diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index fca6c425e2507..d6168cdc37600 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -29,6 +29,7 @@ <arguments> <argument name="factoryMapByConfigElementType" xsi:type="array"> <item name="graphql_interface" xsi:type="object">Magento\Framework\GraphQl\Config\Element\InterfaceFactory</item> + <item name="graphql_union" xsi:type="object">Magento\Framework\GraphQl\Config\Element\UnionFactory</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\InputFactory</item> <item name="graphql_enum" xsi:type="object">Magento\Framework\GraphQl\Config\Element\EnumFactory</item> @@ -64,6 +65,7 @@ <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\UnionType" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Output\OutputUnionObject</item> <item name="Magento\Framework\GraphQl\Config\Element\Enum" xsi:type="string">Magento\Framework\GraphQl\Schema\Type\Enum\Enum</item> </argument> </arguments> @@ -78,6 +80,7 @@ <argument name="formatters" xsi:type="array"> <item name="fields" xsi:type="object">Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\Formatter\Fields</item> <item name="interfaces" xsi:type="object">Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\Formatter\Interfaces</item> + <item name="unions" xsi:type="object">Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\Formatter\Unions</item> <item name="resolveType" xsi:type="object">Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\Formatter\ResolveType</item> </argument> </arguments> @@ -85,6 +88,7 @@ <type name="Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeReaderComposite"> <arguments> <argument name="typeReaders" xsi:type="array"> + <item name="union_type" xsi:type="object">Magento\Framework\GraphQlSchemaStitching\GraphQlReader\Reader\UnionType</item> <item name="enum_type" xsi:type="object">Magento\Framework\GraphQlSchemaStitching\GraphQlReader\Reader\EnumType</item> <item name="object_type" xsi:type="object">Magento\Framework\GraphQlSchemaStitching\GraphQlReader\Reader\ObjectType</item> <item name="input_object_type" xsi:type="object">Magento\Framework\GraphQlSchemaStitching\GraphQlReader\Reader\InputObjectType</item> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index 2595ad09c072a..7366567c2b95d 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -30,14 +30,14 @@ directive @resolver(class: String="") on QUERY | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION - | INTERFACE - | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION -directive @typeResolver(class: String="") on INTERFACE | OBJECT +directive @typeResolver(class: String="") on UNION + | INTERFACE + | OBJECT directive @cache(cacheIdentity: String="" cacheable: Boolean=true) on QUERY diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php index dff6560ebf768..43cc467ad390b 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/Export.php @@ -99,10 +99,10 @@ public function execute() ); } catch (\Exception $e) { $this->_objectManager->get(\Psr\Log\LoggerInterface::class)->critical($e); - $this->messageManager->addError(__('Please correct the data sent value.')); + $this->messageManager->addErrorMessage(__('Please correct the data sent value.')); } } else { - $this->messageManager->addError(__('Please correct the data sent value.')); + $this->messageManager->addErrorMessage(__('Please correct the data sent value.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/GetFilter.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/GetFilter.php index 722d32c9eb21a..789df5dbc466f 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/GetFilter.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/GetFilter.php @@ -35,10 +35,10 @@ public function execute() ); return $resultLayout; } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } else { - $this->messageManager->addError(__('Please correct the data sent value.')); + $this->messageManager->addErrorMessage(__('Please correct the data sent value.')); } /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php index 7c119e1dd683d..9918ef8908956 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Download.php @@ -3,62 +3,71 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ImportExport\Controller\Adminhtml\Import; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Response\Http\FileFactory; use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Controller\Result\Raw; +use Magento\Framework\Controller\Result\RawFactory; use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Filesystem\Directory\ReadFactory; use Magento\ImportExport\Controller\Adminhtml\Import as ImportController; +use Magento\ImportExport\Model\Import\SampleFileProvider; /** * Download sample file controller */ -class Download extends ImportController +class Download extends ImportController implements HttpGetActionInterface { const SAMPLE_FILES_MODULE = 'Magento_ImportExport'; /** - * @var \Magento\Framework\Controller\Result\RawFactory + * @var RawFactory */ protected $resultRawFactory; /** - * @var \Magento\Framework\Filesystem\Directory\ReadFactory + * @var ReadFactory */ protected $readFactory; /** - * @var \Magento\Framework\Component\ComponentRegistrar + * @var ComponentRegistrar */ protected $componentRegistrar; /** - * @var \Magento\Framework\App\Response\Http\FileFactory + * @var FileFactory */ protected $fileFactory; /** - * @var \Magento\ImportExport\Model\Import\SampleFileProvider + * @var SampleFileProvider */ private $sampleFileProvider; /** - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\App\Response\Http\FileFactory $fileFactory - * @param \Magento\Framework\Controller\Result\RawFactory $resultRawFactory - * @param \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory - * @param \Magento\ImportExport\Model\Import\SampleFileProvider $sampleFileProvider + * @param Context $context + * @param FileFactory $fileFactory + * @param RawFactory $resultRawFactory + * @param ReadFactory $readFactory * @param ComponentRegistrar $componentRegistrar - * @param \Magento\ImportExport\Model\Import\SampleFileProvider|null $sampleFileProvider + * @param SampleFileProvider|null $sampleFileProvider */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Framework\App\Response\Http\FileFactory $fileFactory, - \Magento\Framework\Controller\Result\RawFactory $resultRawFactory, - \Magento\Framework\Filesystem\Directory\ReadFactory $readFactory, - \Magento\Framework\Component\ComponentRegistrar $componentRegistrar, - \Magento\ImportExport\Model\Import\SampleFileProvider $sampleFileProvider = null + Context $context, + FileFactory $fileFactory, + RawFactory $resultRawFactory, + ReadFactory $readFactory, + ComponentRegistrar $componentRegistrar, + SampleFileProvider $sampleFileProvider = null ) { parent::__construct( $context @@ -68,14 +77,14 @@ public function __construct( $this->readFactory = $readFactory; $this->componentRegistrar = $componentRegistrar; $this->sampleFileProvider = $sampleFileProvider - ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\ImportExport\Model\Import\SampleFileProvider::class); + ?: ObjectManager::getInstance() + ->get(SampleFileProvider::class); } /** * Download sample file action * - * @return \Magento\Framework\Controller\Result\Raw + * @return Raw */ public function execute() { @@ -89,7 +98,7 @@ public function execute() try { $fileContents = $this->sampleFileProvider->getFileContents($entityName); } catch (NoSuchEntityException $e) { - $this->messageManager->addError(__('There is no sample file for this entity.')); + $this->messageManager->addErrorMessage(__('There is no sample file for this entity.')); return $this->getResultRedirect(); } @@ -105,13 +114,15 @@ public function execute() $fileSize ); - /** @var \Magento\Framework\Controller\Result\Raw $resultRaw */ $resultRaw = $this->resultRawFactory->create(); $resultRaw->setContents($fileContents); + return $resultRaw; } /** + * Get redirect result + * * @return Redirect */ private function getResultRedirect(): Redirect diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php index c18e666260898..4be73fe384ae0 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Import/Validate.php @@ -3,15 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ImportExport\Controller\Adminhtml\Import; +use Magento\Backend\Model\View\Result\Redirect; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\View\Result\Layout; +use Magento\ImportExport\Block\Adminhtml\Import\Frame\Result; 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; /** * Import validate controller action. @@ -27,16 +30,16 @@ class Validate extends ImportResultController implements HttpPostActionInterface /** * Validate uploaded files action * - * @return \Magento\Framework\Controller\ResultInterface - * @SuppressWarnings(PHPMD.Superglobals) + * @return ResultInterface */ public function execute() { $data = $this->getRequest()->getPostValue(); - /** @var \Magento\Framework\View\Result\Layout $resultLayout */ + /** @var Layout $resultLayout */ $resultLayout = $this->resultFactory->create(ResultFactory::TYPE_LAYOUT); /** @var $resultBlock Result */ $resultBlock = $resultLayout->getLayout()->getBlock('import.frame.result'); + //phpcs:disable Magento2.Security.Superglobal if ($data) { // common actions $resultBlock->addAction( @@ -44,7 +47,6 @@ public function execute() 'import_validation_container' ); - /** @var $import \Magento\ImportExport\Model\Import */ $import = $this->getImport()->setData($data); try { $source = $import->uploadFileAndGetSource(); @@ -59,8 +61,8 @@ public function execute() $resultBlock->addError(__('The file was not uploaded.')); return $resultLayout; } - $this->messageManager->addError(__('Sorry, but the data is invalid or the file is not uploaded.')); - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + $this->messageManager->addErrorMessage(__('Sorry, but the data is invalid or the file is not uploaded.')); + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $resultRedirect->setPath('adminhtml/*/index'); return $resultRedirect; @@ -100,7 +102,7 @@ private function processValidationResult($validationResult, $resultBlock) $errorAggregator->getErrorsCount() ) ); - + $this->addErrorMessages($resultBlock, $errorAggregator); } else { if ($errorAggregator->getErrorsCount()) { diff --git a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Import/ValidateTest.php b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Import/ValidateTest.php index 06c89a3e9e543..e54b1e470b54d 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Import/ValidateTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Import/ValidateTest.php @@ -164,7 +164,7 @@ public function testNoDataWasPosted() ]); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('Sorry, but the data is invalid or the file is not uploaded.')); $this->assertEquals($resultRedirectMock, $this->validate->execute()); diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php index 6934284c8e65e..8909fa999528a 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassChangelog.php @@ -22,7 +22,7 @@ public function execute() { $indexerIds = $this->getRequest()->getParam('indexer_ids'); if (!is_array($indexerIds)) { - $this->messageManager->addError(__('Please select indexers.')); + $this->messageManager->addErrorMessage(__('Please select indexers.')); } else { try { foreach ($indexerIds as $indexerId) { @@ -36,7 +36,7 @@ public function execute() __('%1 indexer(s) are in "Update by Schedule" mode.', count($indexerIds)) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addException( $e, diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassInvalidate.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassInvalidate.php index 0cc203a547b3a..2fec3aac698b6 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassInvalidate.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassInvalidate.php @@ -40,7 +40,7 @@ public function execute() { $indexerIds = $this->getRequest()->getParam('indexer_ids'); if (!is_array($indexerIds)) { - $this->messageManager->addError(__('Please select indexers.')); + $this->messageManager->addErrorMessage(__('Please select indexers.')); } else { try { foreach ($indexerIds as $indexerId) { @@ -52,7 +52,7 @@ public function execute() __('%1 indexer(s) were invalidated.', count($indexerIds)) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addException( $e, diff --git a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php index 21fa7a61c621f..f8c3c58f5413b 100644 --- a/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php +++ b/app/code/Magento/Indexer/Controller/Adminhtml/Indexer/MassOnTheFly.php @@ -22,7 +22,7 @@ public function execute() { $indexerIds = $this->getRequest()->getParam('indexer_ids'); if (!is_array($indexerIds)) { - $this->messageManager->addError(__('Please select indexers.')); + $this->messageManager->addErrorMessage(__('Please select indexers.')); } else { try { foreach ($indexerIds as $indexerId) { @@ -36,7 +36,7 @@ public function execute() __('%1 indexer(s) are in "Update on Save" mode.', count($indexerIds)) ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addException( $e, diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminOpenIndexManagementPageActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminOpenIndexManagementPageActionGroup.xml new file mode 100644 index 0000000000000..79732ae4b6f3a --- /dev/null +++ b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/AdminOpenIndexManagementPageActionGroup.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="AdminOpenIndexManagementPageActionGroup"> + <annotations> + <description>Open index management page.</description> + </annotations> + + <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassChangelogTest.php b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassChangelogTest.php index 329e392ea1a8d..a5fc5e7bf68d1 100644 --- a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassChangelogTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassChangelogTest.php @@ -171,7 +171,7 @@ protected function setUp(): void $this->title = $this->createMock(Title::class); $this->messageManager = $this->getMockForAbstractClass( ManagerInterface::class, - ['addError', 'addSuccess'], + ['addErrorMessage', 'addSuccess'], '', false ); @@ -206,7 +206,7 @@ public function testExecute($indexerIds, $exception, $expectsExceptionValues) if (!is_array($indexerIds)) { $this->messageManager->expects($this->once()) - ->method('addError')->with(__('Please select indexers.')) + ->method('addErrorMessage')->with(__('Please select indexers.')) ->willReturn(1); } else { $this->objectManager->expects($this->any()) @@ -235,7 +235,7 @@ public function testExecute($indexerIds, $exception, $expectsExceptionValues) if ($exception !== null) { $this->messageManager ->expects($this->exactly($expectsExceptionValues[2])) - ->method('addError') + ->method('addErrorMessage') ->with($exception->getMessage()); $this->messageManager->expects($this->exactly($expectsExceptionValues[1])) ->method('addException') diff --git a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassInvalidateTest.php b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassInvalidateTest.php index 9c43b8a84d1ba..a49b128681bbc 100644 --- a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassInvalidateTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassInvalidateTest.php @@ -196,7 +196,7 @@ protected function setUp(): void $this->title = $this->createMock(Title::class); $this->messageManager = $this->getMockForAbstractClass( ManagerInterface::class, - ['addError', 'addSuccess'], + ['addErrorMessage', 'addSuccess'], '', false ); @@ -233,7 +233,7 @@ public function testExecute($indexerIds, $exception) if (!is_array($indexerIds)) { $this->messageManager->expects($this->once()) - ->method('addError')->with(__('Please select indexers.')) + ->method('addErrorMessage')->with(__('Please select indexers.')) ->willReturn(1); } else { $indexerInterface = $this->getMockForAbstractClass( @@ -261,7 +261,7 @@ public function testExecute($indexerIds, $exception) if ($exception instanceof LocalizedException) { $this->messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with($exception->getMessage()); } else { $this->messageManager->expects($this->once()) diff --git a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php index 727f5965f9fe4..649db0282d12d 100644 --- a/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Controller/Adminhtml/Indexer/MassOnTheFlyTest.php @@ -171,7 +171,7 @@ protected function setUp(): void $this->title = $this->createMock(Title::class); $this->messageManager = $this->getMockForAbstractClass( ManagerInterface::class, - ['addError', 'addSuccess'], + ['addErrorMessage', 'addSuccess'], '', false ); @@ -206,7 +206,7 @@ public function testExecute($indexerIds, $exception, $expectsExceptionValues) if (!is_array($indexerIds)) { $this->messageManager->expects($this->once()) - ->method('addError')->with(__('Please select indexers.')) + ->method('addErrorMessage')->with(__('Please select indexers.')) ->willReturn(1); } else { $this->objectManager->expects($this->any()) @@ -234,7 +234,7 @@ public function testExecute($indexerIds, $exception, $expectsExceptionValues) if ($exception !== null) { $this->messageManager->expects($this->exactly($expectsExceptionValues[2])) - ->method('addError') + ->method('addErrorMessage') ->with($exception->getMessage()); $this->messageManager->expects($this->exactly($expectsExceptionValues[1])) ->method('addException') diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php index 4ce462bb44c89..1e4f58d0b1250 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Delete.php @@ -27,7 +27,7 @@ public function execute() if ($integrationId) { $integrationData = $this->_integrationService->get($integrationId); if ($this->_integrationData->isConfigType($integrationData)) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __( "Uninstall the extension to remove integration '%1'.", $this->escaper->escapeHtml($integrationData[Info::DATA_NAME]) @@ -37,7 +37,7 @@ public function execute() } $integrationData = $this->_integrationService->delete($integrationId); if (!$integrationData[Info::DATA_ID]) { - $this->messageManager->addError(__('This integration no longer exists.')); + $this->messageManager->addErrorMessage(__('This integration no longer exists.')); } else { //Integration deleted successfully, now safe to delete the associated consumer data if (isset($integrationData[Info::DATA_CONSUMER_ID])) { @@ -52,10 +52,10 @@ public function execute() ); } } else { - $this->messageManager->addError(__('Integration ID is not specified or is invalid.')); + $this->messageManager->addErrorMessage(__('Integration ID is not specified or is invalid.')); } } catch (IntegrationException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->_logger->critical($e); } diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Edit.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Edit.php index 599b6017059e1..25b23065f308e 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Edit.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Edit.php @@ -27,12 +27,12 @@ public function execute() $integrationData = $this->_integrationService->get($integrationId)->getData(); $originalName = $this->escaper->escapeHtml($integrationData[Info::DATA_NAME]); } catch (IntegrationException $e) { - $this->messageManager->addError($this->escaper->escapeHtml($e->getMessage())); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); $this->_redirect('*/*/'); return; } catch (\Exception $e) { $this->_logger->critical($e); - $this->messageManager->addError(__('Internal error. Check exception log for details.')); + $this->messageManager->addErrorMessage(__('Internal error. Check exception log for details.')); $this->_redirect('*/*'); return; } @@ -41,7 +41,7 @@ public function execute() $integrationData = array_merge($integrationData, $restoredIntegration); } } else { - $this->messageManager->addError(__('Integration ID is not specified or is invalid.')); + $this->messageManager->addErrorMessage(__('Integration ID is not specified or is invalid.')); $this->_redirect('*/*/'); return; } diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/PermissionsDialog.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/PermissionsDialog.php index 8b2a94da01d70..e418fa9de1d97 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/PermissionsDialog.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/PermissionsDialog.php @@ -24,17 +24,17 @@ public function execute() $integrationData = $this->_integrationService->get($integrationId)->getData(); $this->_registry->register(self::REGISTRY_KEY_CURRENT_INTEGRATION, $integrationData); } catch (IntegrationException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*/'); return; } catch (\Exception $e) { $this->_logger->critical($e); - $this->messageManager->addError(__('Internal error. Check exception log for details.')); + $this->messageManager->addErrorMessage(__('Internal error. Check exception log for details.')); $this->_redirect('*/*'); return; } } else { - $this->messageManager->addError(__('Integration ID is not specified or is invalid.')); + $this->messageManager->addErrorMessage(__('Integration ID is not specified or is invalid.')); $this->_redirect('*/*/'); return; } diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php index ea255487b9df1..ac237750e7152 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php @@ -69,19 +69,19 @@ public function execute() ); $this->_redirect('*'); } catch (\Magento\Framework\Exception\AuthenticationException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_getSession()->setIntegrationData($this->getRequest()->getPostValue()); $this->_redirectOnSaveError(); } catch (IntegrationException $e) { - $this->messageManager->addError($this->escaper->escapeHtml($e->getMessage())); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); $this->_getSession()->setIntegrationData($this->getRequest()->getPostValue()); $this->_redirectOnSaveError(); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($this->escaper->escapeHtml($e->getMessage())); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); $this->_redirectOnSaveError(); } catch (\Exception $e) { $this->_logger->critical($e); - $this->messageManager->addError($this->escaper->escapeHtml($e->getMessage())); + $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); $this->_redirectOnSaveError(); } } diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensDialog.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensDialog.php index 4c99dafb1d997..f4ebad4954946 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensDialog.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensDialog.php @@ -51,12 +51,12 @@ public function execute() $this->_integrationService->get($integrationId)->getData() ); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*'); return; } catch (\Exception $e) { $this->_logger->critical($e); - $this->messageManager->addError(__('Internal error. Check exception log for details.')); + $this->messageManager->addErrorMessage(__('Internal error. Check exception log for details.')); $this->_redirect('*/*'); return; } diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensExchange.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensExchange.php index a49561dd95ade..2f1884b8db735 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensExchange.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/TokensExchange.php @@ -4,12 +4,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Integration\Controller\Adminhtml\Integration; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Integration\Controller\Adminhtml\Integration; use Magento\Integration\Model\Integration as IntegrationModel; -class TokensExchange extends \Magento\Integration\Controller\Adminhtml\Integration +class TokensExchange extends Integration implements HttpPostActionInterface { /** * Let the admin know that integration has been sent for activation and token exchange is in process. @@ -72,12 +75,12 @@ public function execute() ]; $this->getResponse()->representJson($this->jsonHelper->jsonEncode($result)); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*'); return; } catch (\Exception $e) { $this->_logger->critical($e); - $this->messageManager->addError(__('Internal error. Check exception log for details.')); + $this->messageManager->addErrorMessage(__('Internal error. Check exception log for details.')); $this->_redirect('*/*'); return; } diff --git a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/DeleteTest.php b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/DeleteTest.php index aa1393be6534c..074c6ff2c2ae2 100644 --- a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/DeleteTest.php +++ b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/DeleteTest.php @@ -120,7 +120,7 @@ public function testDeleteActionConfigSetUp() ->willReturn(true); // verify error message $this->_messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('Uninstall the extension to remove integration \'%1\'.', $intData[Info::DATA_NAME])); $this->_integrationSvcMock->expects($this->never())->method('delete'); // Use real translate model @@ -143,7 +143,7 @@ public function testDeleteActionMissingId() $this->_translateModelMock = null; // verify error message $this->_messageManager->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with(__('Integration ID is not specified or is invalid.')); $this->integrationController->execute(); @@ -166,7 +166,7 @@ public function testDeleteActionForServiceIntegrationException() $this->_integrationSvcMock->expects($this->once()) ->method('delete') ->willThrowException($invalidIdException); - $this->_messageManager->expects($this->once())->method('addError'); + $this->_messageManager->expects($this->once())->method('addErrorMessage'); $this->integrationController->execute(); } @@ -188,7 +188,7 @@ public function testDeleteActionForServiceGenericException() $this->_integrationSvcMock->expects($this->once()) ->method('delete') ->willThrowException($invalidIdException); - $this->_messageManager->expects($this->never())->method('addError'); + $this->_messageManager->expects($this->never())->method('addErrorMessage'); $this->integrationController->execute(); } diff --git a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/EditTest.php b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/EditTest.php index 5cfa8b290b6b9..a9dc8ec616674 100644 --- a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/EditTest.php +++ b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/EditTest.php @@ -62,7 +62,7 @@ public function testEditActionNonExistentIntegration() { $exceptionMessage = 'This integration no longer exists.'; // verify the error - $this->_messageManager->expects($this->once())->method('addError')->with($exceptionMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($exceptionMessage); $this->_requestMock->expects($this->any())->method('getParam')->willReturn(self::INTEGRATION_ID); // put data in session, the magic function getFormData is called so, must match __call method name $this->_backendSessionMock->expects( @@ -93,7 +93,7 @@ public function testEditActionNoDataAdd() { $exceptionMessage = 'Integration ID is not specified or is invalid.'; // verify the error - $this->_messageManager->expects($this->once())->method('addError')->with($exceptionMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($exceptionMessage); $this->_verifyLoadAndRenderLayout(); $integrationContr = $this->_createIntegrationController('Edit'); $integrationContr->execute(); @@ -103,7 +103,7 @@ public function testEditException() { $exceptionMessage = 'Integration ID is not specified or is invalid.'; // verify the error - $this->_messageManager->expects($this->once())->method('addError')->with($exceptionMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($exceptionMessage); $this->_controller = $this->_createIntegrationController('Edit'); $this->_controller->execute(); } diff --git a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/SaveTest.php b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/SaveTest.php index 8de8b45833043..f3b0c65b6a706 100644 --- a/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/SaveTest.php +++ b/app/code/Magento/Integration/Test/Unit/Controller/Adminhtml/Integration/SaveTest.php @@ -180,7 +180,7 @@ public function testSaveActionExceptionDuringServiceCreation() // Use real translate model $this->_translateModelMock = null; // Verify success message - $this->_messageManager->expects($this->once())->method('addError')->with($exceptionMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($exceptionMessage); $integrationController = $this->_createIntegrationController('Save'); $integrationController->execute(); } @@ -211,7 +211,7 @@ public function testSaveActionExceptionOnIntegrationsCreatedFromConfigFile() ->willReturnArgument(0); // Verify error - $this->_messageManager->expects($this->once())->method('addError')->with($exceptionMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($exceptionMessage); $integrationContr = $this->_createIntegrationController('Save'); $integrationContr->execute(); } @@ -283,7 +283,7 @@ public function testSaveActionAuthenticationException() ->willThrowException(new AuthenticationException(__($exceptionMessage))); // Verify error - $this->_messageManager->expects($this->once())->method('addError')->with($exceptionMessage); + $this->_messageManager->expects($this->once())->method('addErrorMessage')->with($exceptionMessage); $integrationContr = $this->_createIntegrationController('Save'); $integrationContr->execute(); } diff --git a/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php index 55f99697c289b..e591b4f2339b1 100644 --- a/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php +++ b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php @@ -7,8 +7,6 @@ namespace Magento\MediaContentSynchronization\Console\Command; -use Magento\Framework\App\Area; -use Magento\Framework\App\State; use Magento\Framework\Console\Cli; use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; use Symfony\Component\Console\Command\Command; @@ -25,21 +23,13 @@ class Synchronize extends Command */ private $synchronizeContent; - /** - * @var State $state - */ - private $state; - /** * @param SynchronizeInterface $synchronizeContent - * @param State $state */ public function __construct( - SynchronizeInterface $synchronizeContent, - State $state + SynchronizeInterface $synchronizeContent ) { $this->synchronizeContent = $synchronizeContent; - $this->state = $state; parent::__construct(); } @@ -58,12 +48,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln('Synchronizing content with assets...'); - $this->state->emulateAreaCode( - Area::AREA_ADMINHTML, - function () { - $this->synchronizeContent->execute(); - } - ); + $this->synchronizeContent->execute(); $output->writeln('Completed content synchronization.'); return Cli::RETURN_SUCCESS; } diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php index bcce3514e4ad9..b01c02cae4234 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Consume.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -7,6 +7,11 @@ namespace Magento\MediaContentSynchronization\Model; +use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; /** @@ -14,24 +19,73 @@ */ class Consume { + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + /** * @var SynchronizeInterface */ private $synchronize; /** + * @var SynchronizeIdentitiesInterface + */ + private $synchronizeIdentities; + + /** + * @param SerializerInterface $serializer + * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param SynchronizeInterface $synchronize + * @param SynchronizeIdentitiesInterface $synchronizeIdentities */ - public function __construct(SynchronizeInterface $synchronize) - { + public function __construct( + SerializerInterface $serializer, + ContentIdentityInterfaceFactory $contentIdentityFactory, + SynchronizeInterface $synchronize, + SynchronizeIdentitiesInterface $synchronizeIdentities + ) { + $this->serializer = $serializer; + $this->contentIdentityFactory = $contentIdentityFactory; $this->synchronize = $synchronize; + $this->synchronizeIdentities = $synchronizeIdentities; } /** * Run media files synchronization. + * + * @param OperationInterface $operation + * @throws LocalizedException */ - public function execute() : void + public function execute(OperationInterface $operation) : void { - $this->synchronize->execute(); + $identities = $this->serializer->unserialize($operation->getSerializedData()); + + if (empty($identities)) { + $this->synchronize->execute(); + return; + } + + $contentIdentities = []; + foreach ($identities as $identity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $identity[self::ENTITY_TYPE], + self::ENTITY_ID => $identity[self::ENTITY_ID], + self::FIELD => $identity[self::FIELD] + ] + ); + } + $this->synchronizeIdentities->execute($contentIdentities); } } diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php index ad6fdd27d7067..d9e89fea7d4d2 100644 --- a/app/code/Magento/MediaContentSynchronization/Model/Publish.php +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -7,7 +7,11 @@ namespace Magento\MediaContentSynchronization\Model; +use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\Framework\Bulk\OperationInterface; +use Magento\Framework\DataObject\IdentityGeneratorInterface; use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\Framework\Serialize\SerializerInterface; /** * Publish media content synchronization queue. @@ -19,27 +23,64 @@ class Publish */ private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization'; + /** + * @var OperationInterfaceFactory + */ + private $operationFactory; + + /** + * @var IdentityGeneratorInterface + */ + private $identityService; + /** * @var PublisherInterface */ private $publisher; /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @param OperationInterfaceFactory $operationFactory + * @param IdentityGeneratorInterface $identityService * @param PublisherInterface $publisher + * @param SerializerInterface $serializer */ - public function __construct(PublisherInterface $publisher) - { + public function __construct( + OperationInterfaceFactory $operationFactory, + IdentityGeneratorInterface $identityService, + PublisherInterface $publisher, + SerializerInterface $serializer + ) { + $this->operationFactory = $operationFactory; + $this->identityService = $identityService; + $this->serializer = $serializer; $this->publisher = $publisher; } /** - * Publish media content synchronization message to the message queue. + * Publish media content synchronization message to the message queue + * + * @param array $contentIdentities */ - public function execute() : void + public function execute(array $contentIdentities = []) : void { + $data = [ + 'data' => [ + 'bulk_uuid' => $this->identityService->generateId(), + 'topic_name' => self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, + 'serialized_data' => $this->serializer->serialize($contentIdentities), + 'status' => OperationInterface::STATUS_TYPE_OPEN, + ] + ]; + $operation = $this->operationFactory->create($data); + $this->publisher->publish( self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, - [self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION] + $operation ); } } diff --git a/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php new file mode 100644 index 0000000000000..1bf57c6b2ec42 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/SynchronizeIdentities.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\FlagManager; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; +use Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool; +use Psr\Log\LoggerInterface; + +/** + * Batch Synchronize content with assets + */ +class SynchronizeIdentities implements SynchronizeIdentitiesInterface +{ + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var SynchronizeIdentitiesPool + */ + private $synchronizeIdentitiesPool; + + /** + * @param LoggerInterface $log + * @param SynchronizeIdentitiesPool $synchronizeIdentitiesPool + */ + public function __construct( + LoggerInterface $log, + SynchronizeIdentitiesPool $synchronizeIdentitiesPool + ) { + $this->log = $log; + $this->synchronizeIdentitiesPool = $synchronizeIdentitiesPool; + } + + /** + * @inheritdoc + */ + public function execute(array $mediaContentIdentities): void + { + $failed = []; + + foreach ($this->synchronizeIdentitiesPool->get() as $name => $synchronizer) { + try { + $synchronizer->execute($mediaContentIdentities); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following content synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php new file mode 100644 index 0000000000000..2314796481b55 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Test/Integration/Model/PublisherTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Test\Integration\Model; + +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\MessageQueue\ConsumerFactory; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronization\Model\Publish; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for media content Publisher + */ +class PublisherTest extends TestCase +{ + private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization'; + + /** + * @var ConsumerFactory + */ + private $consumerFactory; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var Publish + */ + private $publish; + + protected function setUp(): void + { + $this->consumerFactory = Bootstrap::getObjectManager()->get(ConsumerFactory::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->publish = Bootstrap::getObjectManager()->get(Publish::class); + } + + /** + * @dataProvider filesProvider + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @param array $contentIdentities + * @throws IntegrationException + * @throws LocalizedException + */ + public function testExecute(array $contentIdentities): void + { + // publish message to the queue + $this->publish->execute($contentIdentities); + + // run and process message + $batchSize = 1; + $maxNumberOfMessages = 1; + $consumer = $this->consumerFactory->get(self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, $batchSize); + $consumer->process($maxNumberOfMessages); + + // verify synchronized media content + $assetId = 2020; + $entityIds = []; + foreach ($contentIdentities as $contentIdentity) { + $contentIdentityObject = $this->contentIdentityFactory->create($contentIdentity); + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentityObject)); + $entityIds[] = $contentIdentityObject->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); + } + } + + /** + * Data provider + * + * @return array + */ + public function filesProvider(): array + { + return [ + [ + [ + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => 28767 + ], + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => 1567 + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json index 3be5f535487ec..9f0f4f9588ad6 100644 --- a/app/code/Magento/MediaContentSynchronization/composer.json +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -4,9 +4,10 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/framework-bulk": "*", "magento/module-media-content-synchronization-api": "*", - "magento/framework-message-queue": "*", - "magento/module-media-content-api": "*" + "magento/module-media-content-api": "*", + "magento/module-asynchronous-operations": "*" }, "suggest": { "magento/module-media-gallery-synchronization": "*" diff --git a/app/code/Magento/MediaContentSynchronization/etc/communication.xml b/app/code/Magento/MediaContentSynchronization/etc/communication.xml index e3436aee85331..05641b7432564 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/communication.xml +++ b/app/code/Magento/MediaContentSynchronization/etc/communication.xml @@ -7,7 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> - <topic name="media.content.synchronization" is_synchronous="false" request="string[]"> + <topic name="media.content.synchronization" is_synchronous="false" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"> <handler name="media.content.synchronization.handler" type="Magento\MediaContentSynchronization\Model\Consume" method="execute"/> </topic> diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml index d4615c15206e5..e5347f1a11561 100644 --- a/app/code/Magento/MediaContentSynchronization/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronization/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\MediaContentSynchronizationApi\Api\SynchronizeInterface" type="Magento\MediaContentSynchronization\Model\Synchronize"/> + <preference for="Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface" type="Magento\MediaContentSynchronization\Model\SynchronizeIdentities"/> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> diff --git a/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php new file mode 100644 index 0000000000000..7e21cbb570053 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeIdentitiesInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Api; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; + +/** + * Synchronize bulk assets and contents + */ +interface SynchronizeIdentitiesInterface +{ + /** + * Synchronize media contents + * + * @param ContentIdentityInterface[] $contentIdentities + */ + public function execute(array $contentIdentities): void; +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php new file mode 100644 index 0000000000000..1ea957d5cd6e7 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizeIdentitiesPool.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Model; + +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; + +class SynchronizeIdentitiesPool +{ + /** + * Content with assets synchronizers + * + * @var SynchronizeIdentitiesInterface[] + */ + private $synchronizers; + + /** + * @param SynchronizeIdentitiesInterface[] $synchronizers + */ + public function __construct( + array $synchronizers = [] + ) { + foreach ($synchronizers as $synchronizer) { + if (!$synchronizer instanceof SynchronizeIdentitiesInterface) { + throw new \InvalidArgumentException( + get_class($synchronizer) . ' must implement ' . SynchronizeIdentitiesInterface::class + ); + } + } + + $this->synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizeIdentitiesInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json index 1f1e5e4b51c5b..398aaf1de8071 100644 --- a/app/code/Magento/MediaContentSynchronizationApi/composer.json +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -3,7 +3,8 @@ "description": "Magento module responsible for the media content synchronization implementation API", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-media-content-api": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php new file mode 100644 index 0000000000000..77188b65a8b88 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/SynchronizeIdentities.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Model\Synchronizer; + +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; + +class SynchronizeIdentities implements SynchronizeIdentitiesInterface +{ + private const FIELD_CATALOG_PRODUCT = 'catalog_product'; + private const FIELD_CATALOG_CATEGORY = 'catalog_category'; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param GetEntityContentsInterface $getEntityContents + */ + public function __construct( + UpdateContentAssetLinksInterface $updateContentAssetLinks, + GetEntityContentsInterface $getEntityContents + ) { + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->getEntityContents = $getEntityContents; + } + + /** + * @inheritDoc + */ + public function execute(array $mediaContentIdentities): void + { + foreach ($mediaContentIdentities as $identity) { + if ($identity->getEntityType() === self::FIELD_CATALOG_PRODUCT + || $identity->getEntityType() === self::FIELD_CATALOG_CATEGORY + ) { + $this->updateContentAssetLinks->execute( + $identity, + implode(PHP_EOL, $this->getEntityContents->execute($identity)) + ); + } + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php new file mode 100644 index 0000000000000..5be72e2b4bf60 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Test\Integration\Model\Synchronizer; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for catalog SynchronizeIdentities. + */ +class SynchronizeIdentitiesTest extends TestCase +{ + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var SynchronizeIdentitiesInterface + */ + private $synchronizeIdentities; + + protected function setUp(): void + { + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + } + + /** + * @dataProvider filesProvider + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @param ContentIdentityInterface[] $mediaContentIdentities + * @throws IntegrationException + */ + public function testExecute(array $mediaContentIdentities): void + { + $assetId = 2020; + + $contentIdentities = []; + foreach ($mediaContentIdentities as $mediaContentIdentity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $mediaContentIdentity[self::ENTITY_TYPE], + self::ENTITY_ID => $mediaContentIdentity[self::ENTITY_ID], + self::FIELD => $mediaContentIdentity[self::FIELD] + ] + ); + } + + $this->assertNotEmpty($contentIdentities); + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->synchronizeIdentities->execute($contentIdentities); + + $entityIds = []; + foreach ($contentIdentities as $contentIdentity) { + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + $entityIds[] = $contentIdentity->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); + } + } + + /** + * Data provider + * + * @return array + */ + public function filesProvider(): array + { + return [ + [ + [ + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => 28767 + ], + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => 1567 + ] + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml index 8cc86fde8fbcd..070f25f501712 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -38,4 +38,13 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_catalog" + xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\SynchronizeIdentities + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php new file mode 100644 index 0000000000000..7dd2596a910de --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/SynchronizeIdentities.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Model\Synchronizer; + +use Magento\Framework\App\ResourceConnection; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; + +class SynchronizeIdentities implements SynchronizeIdentitiesInterface +{ + private const FIELD_CMS_PAGE = 'cms_page'; + private const FIELD_CMS_BLOCK = 'cms_block'; + private const ID_CMS_PAGE = 'page_id'; + private const ID_CMS_BLOCK = 'block_id'; + private const COLUMN_CMS_CONTENT = 'content'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @param ResourceConnection $resourceConnection + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param GetEntityContentsInterface $getEntityContents + */ + public function __construct( + ResourceConnection $resourceConnection, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + GetEntityContentsInterface $getEntityContents + ) { + $this->resourceConnection = $resourceConnection; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->getEntityContents = $getEntityContents; + } + + /** + * @inheritDoc + */ + public function execute(array $mediaContentIdentities): void + { + foreach ($mediaContentIdentities as $identity) { + if ($identity->getEntityType() === self::FIELD_CMS_PAGE + || $identity->getEntityType() === self::FIELD_CMS_BLOCK + ) { + $this->updateContentAssetLinks->execute( + $identity, + $this->getCmsMediaContent($identity->getEntityType(), (int)$identity->getEntityId()) + ); + } + } + } + + /** + * Get cms media content from database + * + * @param string $tableName + * @param int $cmsId + * @return string + */ + private function getCmsMediaContent(string $tableName, int $cmsId): string + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName($tableName); + $idField = $tableName == self::FIELD_CMS_BLOCK ? $idField = self::ID_CMS_BLOCK : self::ID_CMS_PAGE; + + $select = $connection->select() + ->from($tableName, self::COLUMN_CMS_CONTENT) + ->where($idField . '= ?', $cmsId); + $data = $connection->fetchOne($select); + + return (string)$data; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php new file mode 100644 index 0000000000000..825542baaff8c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/SynchronizeIdentitiesTest.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Test\Integration\Model\Synchronizer; + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\IntegrationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeIdentitiesInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for CMS SynchronizeIdentities. + */ +class SynchronizeIdentitiesTest extends TestCase +{ + private const ENTITY_TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var SynchronizeIdentitiesInterface + */ + private $synchronizeIdentities; + + protected function setUp(): void + { + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->synchronizeIdentities = Bootstrap::getObjectManager()->get(SynchronizeIdentitiesInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + } + + /** + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @throws IntegrationException + * @throws LocalizedException + */ + public function testExecute(): void + { + $assetId = 2020; + $pageId = $this->getPage('fixture_page_with_asset')->getId(); + $blockId = $this->getBlock('fixture_block_with_asset')->getId(); + $mediaContentIdentities = [ + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => $pageId + ], + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => $blockId + ] + ]; + + $contentIdentities = []; + foreach ($mediaContentIdentities as $mediaContentIdentity) { + $contentIdentities[] = $this->contentIdentityFactory->create( + [ + self::ENTITY_TYPE => $mediaContentIdentity[self::ENTITY_TYPE], + self::ENTITY_ID => $mediaContentIdentity[self::ENTITY_ID], + self::FIELD => $mediaContentIdentity[self::FIELD] + ] + ); + } + + $this->assertNotEmpty($contentIdentities); + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->synchronizeIdentities->execute($contentIdentities); + + $entityIds = []; + foreach ($contentIdentities as $contentIdentity) { + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + $entityIds[] = $contentIdentity->getEntityId(); + } + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertContains($syncedContentIdentity->getEntityId(), $entityIds); + } + } + + /** + * Get fixture block + * + * @param string $identifier + * @return BlockInterface + * @throws LocalizedException + */ + private function getBlock(string $identifier): BlockInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var BlockRepositoryInterface $blockRepository */ + $blockRepository = $objectManager->get(BlockRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, $identifier) + ->create(); + + return current($blockRepository->getList($searchCriteria)->getItems()); + } + + /** + * Get fixture page + * + * @param string $identifier + * @return PageInterface + * @throws LocalizedException + */ + private function getPage(string $identifier): PageInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var PageRepositoryInterface $repository */ + $repository = $objectManager->get(PageRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, $identifier) + ->create(); + + return current($repository->getList($searchCriteria)->getItems()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml index 7def330298789..d6e7604c71d97 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -14,6 +14,15 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizeIdentitiesPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_cms" + xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\SynchronizeIdentities + </item> + </argument> + </arguments> + </type> <type name="Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface"> <arguments> <argument name="entities" xsi:type="array"> diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php index f33c22a18b4b8..d0ba786c7084e 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php @@ -78,7 +78,7 @@ public function execute(array $paths): void if (!empty($failedPaths)) { throw new CouldNotSaveException( __( - 'Could not save directories: %paths', + 'Could not create directories: %paths', [ 'paths' => implode(' ,', $failedPaths) ] diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php index eb6bd2aad236c..87f9359d4fc37 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php @@ -133,7 +133,7 @@ private function deleteAssetKeywords(int $assetId, array $obsoleteKeywordIds): v /** @var Mysql $connection */ $connection = $this->resourceConnection->getConnection(); $connection->delete( - $connection->getTableName( + $this->resourceConnection->getTableName( self::TABLE_ASSET_KEYWORD ), [ @@ -196,7 +196,7 @@ private function setAssetUpdatedAt(int $assetId): void try { $connection = $this->resourceConnection->getConnection(); $connection->update( - $connection->getTableName(self::TABLE_MEDIA_ASSET), + $this->resourceConnection->getTableName(self::TABLE_MEDIA_ASSET), ['updated_at' => null], ['id =?' => $assetId] ); diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/ResourceModel/Keyword/SaveAssetLinksTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/ResourceModel/Keyword/SaveAssetLinksTest.php index d027f0ed21b53..6531cddf628df 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/ResourceModel/Keyword/SaveAssetLinksTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/ResourceModel/Keyword/SaveAssetLinksTest.php @@ -78,10 +78,14 @@ public function testAssetKeywordsSave(int $assetId, array $keywordIds, array $va $this->resourceConnectionMock->expects($this->exactly(2)) ->method('getConnection') ->willReturn($this->connectionMock); - $this->resourceConnectionMock->expects($this->once()) + $this->resourceConnectionMock->expects($this->any()) ->method('getTableName') - ->with('media_gallery_asset_keyword') - ->willReturn('prefix_media_gallery_asset_keyword'); + ->willReturnMap( + [ + ['media_gallery_asset_keyword', 'default', 'prefix_media_gallery_asset_keyword'], + ['media_gallery_asset', 'default', 'prefix_media_gallery_asset'] + ] + ); $this->connectionMock->expects($this->once()) ->method('insertArray') ->with( diff --git a/app/code/Magento/MediaGallery/etc/db_schema.xml b/app/code/Magento/MediaGallery/etc/db_schema.xml index 1001737daa8a7..1a9b0dc96a655 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema.xml +++ b/app/code/Magento/MediaGallery/etc/db_schema.xml @@ -8,7 +8,7 @@ <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="media_gallery_asset" resource="default" engine="innodb" comment="Media Gallery Asset"> <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> - <column xsi:type="varchar" name="path" length="255" nullable="true" comment="Path"/> + <column xsi:type="text" name="path" nullable="true" comment="Path"/> <column xsi:type="varchar" name="title" length="255" nullable="true" comment="Title"/> <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="source" length="255" nullable="true" comment="Source"/> @@ -25,9 +25,6 @@ <index referenceId="MEDIA_GALLERY_ID" indexType="btree"> <column name="id"/> </index> - <constraint xsi:type="unique" referenceId="MEDIA_GALLERY_ID_PATH_TITLE_CONTENT_TYPE_WIDTH_HEIGHT"> - <column name="path"/> - </constraint> <index referenceId="MEDIA_GALLERY_ASSET_TITLE" indexType="fulltext"> <column name="title"/> </index> diff --git a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json index b32dfbf082175..e958d630b7e3f 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json +++ b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json @@ -20,7 +20,6 @@ "MEDIA_GALLERY_ASSET_TITLE": true }, "constraint": { - "MEDIA_GALLERY_ID_PATH_TITLE_CONTENT_TYPE_WIDTH_HEIGHT": true, "PRIMARY": true, "MEDIA_GALLERY_ASSET_PATH": true } diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php index d439b53c120cb..b683ec8fe9d91 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php @@ -81,17 +81,18 @@ public function __construct( * * @param ImageUploader $subject * @param string $imagePath + * @param string $initialImageName * @return string * @throws LocalizedException */ - public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath): string + public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath, string $initialImageName): string { if (!$this->config->isEnabled()) { return $imagePath; } $absolutePath = $this->storage->getCmsWysiwygImages()->getStorageRoot() . $imagePath; - $tmpPath = $subject->getBaseTmpPath() . '/' . substr(strrchr($imagePath, '/'), 1); + $tmpPath = $subject->getBaseTmpPath() . '/' . $initialImageName; $tmpAssets = $this->getAssetsByPaths->execute([$tmpPath]); if (!empty($tmpAssets)) { diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.xml new file mode 100644 index 0000000000000..8add2021f056b --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Test/Mftf/Test/AdminUploadSameImageDeleteFromTemporaryFolderTest.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="AdminUploadSameImageDeleteFromTemporaryFolderTest"> + <annotations> + <features value="AdminUploadSameImageDeleteFromTemporaryFolderTest"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1792"/> + <title value="Image is deleted from tmp folder if is uploaded second time"/> + <description value="Image is deleted from tmp folder if is uploaded second time"/> + <stories value="Image is deleted from tmp folder if is uploaded second time"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4836631"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + + <!-- Upload test image to category twice --> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImageSecondTime"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryFormSecondTime"/> + + <!-- Open tmp/category folder --> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup" stepKey="expandTmpFolder"/> + <actionGroup ref="AdminMediaGalleryFolderSelectByFullPathActionGroup" stepKey="selectCategoryFolder"> + <argument name="path" value="catalog/tmp/category"/> + </actionGroup> + + <!-- Assert folder is empty --> + <actionGroup ref="AdminAssertMediaGalleryEmptyActionGroup" stepKey="assertEmptyFolder"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php new file mode 100644 index 0000000000000..f70d4584547a3 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogUi\Controller\Adminhtml\Product; + +use Magento\Framework\Controller\ResultInterface; +use Magento\Backend\App\Action\Context; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Backend\App\Action; + +/** + * Returns selected product by product id. for ui-select filter + */ +class GetSelected extends Action implements HttpGetActionInterface +{ + /** + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Catalog::products'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * GetSelected constructor. + * + * @param JsonFactory $jsonFactory + * @param ProductRepositoryInterface $productRepository + * @param Context $context + */ + public function __construct( + JsonFactory $jsonFactory, + ProductRepositoryInterface $productRepository, + Context $context + ) { + $this->resultJsonFactory = $jsonFactory; + $this->productRepository = $productRepository; + parent::__construct($context); + } + + /** + * Return selected products options + * + * @return ResultInterface + */ + public function execute() : ResultInterface + { + $productIds = $this->getRequest()->getParam('ids'); + $options = []; + + if (!is_array($productIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + } + foreach ($productIds as $id) { + try { + $product = $this->productRepository->getById($id); + $options[] = [ + 'value' => $product->getId(), + 'label' => $product->getName(), + 'is_active' => $product->getSatus(), + 'path' => $product->getSku() + ]; + } catch (\Exception $e) { + continue; + } + } + + return $this->resultJsonFactory->create()->setData($options); + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml deleted file mode 100644 index 884fa47152932..0000000000000 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml +++ /dev/null @@ -1,23 +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="AdminAssertCategoryGridPageDetailsActionGroup"> - <annotations> - <description>Assert category grid page basic columns values for default category</description> - </annotations> - - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.image('1','image')}}" stepKey="assertImageColumn"/> - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.path('1')}}" stepKey="assertPathColumn"/> - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.name('1', 'Default Category')}}" stepKey="assertNameColumn"/> - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.displayMode('1', 'PRODUCTS')}}" stepKey="assertDisplayModeColumn"/> - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.products('1', '0')}}" stepKey="assertProductsColumn"/> - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.inMenu('1', 'Yes')}}" stepKey="assertInMenuColumn"/> - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.enabled('1', 'Yes')}}" stepKey="assertEnabledColumn"/> - </actionGroup> -</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup.xml new file mode 100644 index 0000000000000..e21fa89965391 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup.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="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup"> + <annotations> + <description>Assert asset filter placeholder value</description> + </annotations> + <arguments> + <argument name="filterPlaceholder" type="string"/> + </arguments> + + <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="{{filterPlaceholder}}" stepKey="seeFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminSearchCategoryGridPageByCategoryNameActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminSearchCategoryGridPageByCategoryNameActionGroup.xml new file mode 100644 index 0000000000000..7c3a0165c28d0 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminSearchCategoryGridPageByCategoryNameActionGroup.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="AdminSearchCategoryGridPageByCategoryNameActionGroup"> + <annotations> + <description>Fills 'Search by category name' on Category Grid page. Clicks on Submit Search.</description> + </annotations> + <arguments> + <argument name="categoryName"/> + </arguments> + + <conditionalClick selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.clearFilters}}" dependentSelector="{{AdminMediaGalleryCatalogUiCategoryGridSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> + <fillField selector="{{AdminMediaGalleryCatalogUiCategoryGridSearchSection.searchInput}}" userInput="{{categoryName}}" stepKey="fillKeywordSearchField"/> + <click selector="{{AdminMediaGalleryCatalogUiCategoryGridSearchSection.submitSearch}}" stepKey="clickKeywordSearch"/> + <waitForLoadingMaskToDisappear stepKey="waitingForLoading" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageDetailsActionGroup.xml new file mode 100644 index 0000000000000..cec17bdbb1428 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageDetailsActionGroup.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="AssertAdminCategoryGridPageDetailsActionGroup"> + <arguments> + <argument name="category"/> + </arguments> + <annotations> + <description>Assert category grid page name and path column values for a specific category</description> + </annotations> + + <grabTextFrom selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.columnValue('Name')}}" stepKey="grabNameColumnValue"/> + <assertEquals stepKey="assertNameColumn"> + <expectedResult type="string">$$category.name$$</expectedResult> + <actualResult type="variable">grabNameColumnValue</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.columnValue('Path')}}" stepKey="grabPathColumnValue"/> + <assertStringContainsString stepKey="assertPathColumn"> + <expectedResult type="string">$$category.name$$</expectedResult> + <actualResult type="variable">grabPathColumnValue</actualResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageImageColumnActionGroup.xml similarity index 59% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml rename to app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageImageColumnActionGroup.xml index 42d723f0811d3..b110ce44a8469 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageImageColumnActionGroup.xml @@ -5,17 +5,16 @@ * See COPYING.txt for license details. */ --> - <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup"> - <annotations> - <description>Asserts category name in category grid page</description> - </annotations> + <actionGroup name="AssertAdminCategoryGridPageImageColumnActionGroup"> <arguments> - <argument name="categoryName" type="string"/> + <argument name="file" type="string" defaultValue="magento"/> </arguments> + <annotations> + <description>Assert category grid page image column a specific category</description> + </annotations> - <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.name('1', categoryName)}}" stepKey="assertNameColumn"/> - </actionGroup> + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.image(file)}}" stepKey="assertImageColumn"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageNumberOfRecordsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageNumberOfRecordsActionGroup.xml new file mode 100644 index 0000000000000..72b1bca56cb6e --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageNumberOfRecordsActionGroup.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="AssertAdminCategoryGridPageNumberOfRecordsActionGroup"> + <arguments> + <argument name="numberOfRecords" type="string"/> + </arguments> + <annotations> + <description>Assert the number of records in the category grid page.</description> + </annotations> + + <grabTextFrom selector="{{AdminMediaGalleryCatalogUiCategoryGridSearchSection.numberOfRecordsFound}}" stepKey="grabNumberOfRecordsFound"/> + <assertEquals stepKey="assertStringIsEqual"> + <expectedResult type="string">{{numberOfRecords}}</expectedResult> + <actualResult type="variable">grabNumberOfRecordsFound</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup.xml new file mode 100644 index 0000000000000..e5d6f26e777fc --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup.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="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup"> + <annotations> + <description>Assert category grid page products, in menu, and enabled column values for a specific category</description> + </annotations> + + <grabTextFrom selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.columnValue('Products')}}" stepKey="grabProductsColumnValue"/> + <assertEquals stepKey="assertProductsColumn"> + <expectedResult type="string">0</expectedResult> + <actualResult type="variable">grabProductsColumnValue</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.columnValue('In Menu')}}" stepKey="grabInMenuColumnValue"/> + <assertEquals stepKey="assertInMenuColumn"> + <expectedResult type="string">Yes</expectedResult> + <actualResult type="variable">grabInMenuColumnValue</actualResult> + </assertEquals> + <grabTextFrom selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.columnValue('Enabled')}}" stepKey="grabEnabledColumnValue"/> + <assertEquals stepKey="assertEnabledColumn"> + <expectedResult type="string">Yes</expectedResult> + <actualResult type="variable">grabEnabledColumnValue</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml index 99cee48f443c7..59775dd148712 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml @@ -7,6 +7,7 @@ --> <pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> <page name="AdminMediaGalleryCatalogUiCategoryGridPage" url="media_gallery_catalog/category/index" area="admin" module="Magento_MediaGalleryCatalogUi"> + <section name="AdminMediaGalleryCatalogUiCategoryGridSearchSection"/> <section name="AdminMediaGalleryCatalogUiCategoryGridSection"/> </page> </pages> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSearchSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSearchSection.xml new file mode 100644 index 0000000000000..867721d1e42bb --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSearchSection.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="AdminMediaGalleryCatalogUiCategoryGridSearchSection"> + <element name="searchInput" type="input" selector=".admin__data-grid-header input[placeholder='Search by category name']"/> + <element name="submitSearch" type="button" selector=".data-grid-search-control-wrap > button.action-submit" timeout="30"/> + <element name="numberOfRecordsFound" type="text" selector=".admin__data-grid-header .admin__control-support-text"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml index b787f6feaf61e..f65ec84bc2ec8 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -9,14 +9,10 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminMediaGalleryCatalogUiCategoryGridSection"> + <element name="clearFilters" type="button" selector=".admin__data-grid-header [data-action='grid-filter-reset']" timeout="30"/> <element name="activeFilterPlaceholder" type="text" selector="//div[@class='admin__current-filters-list-wrap']//li//span[contains(text(), '{{filterPlaceholder}}')]" parameterized="true"/> - <element name="image" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Image')]/preceding-sibling::th) +1]//img[contains(@src, '{{imageName}}')]" parameterized="true"/> - <element name="path" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Path')]/preceding-sibling::th)]" parameterized="true"/> - <element name="name" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Name')]/preceding-sibling::th) +1 ]//*[text()='{{categoryName}}']" parameterized="true"/> - <element name="displayMode" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Display Mode')]/preceding-sibling::th) +1 ]//*[text()='{{productsText}}']" parameterized="true"/> - <element name="products" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Products')]/preceding-sibling::th) +1 ]//*[text()='{{productsQty}}']" parameterized="true"/> + <element name="image" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Image')]/preceding-sibling::th) +1]//img[contains(@src, '{{file}}')]" parameterized="true"/> + <element name="columnValue" type="text" selector="//tr//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{columnName}}')]/preceding-sibling::th) +1 ]//div" parameterized="true"/> <element name="edit" type="button" selector="//tr[td//text()[contains(., '{{categoryName}}')]]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Action')]/preceding-sibling::th) +1 ]//*[text()='{{actionButton}}']" parameterized="true"/> - <element name="inMenu" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'In Menu')]/preceding-sibling::th) +1 ]//*[text()='{{inMenuValue}}']" parameterized="true"/> - <element name="enabled" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Enabled')]/preceding-sibling::th) +1 ]//*[text()='{{enabledValue}}']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml index 74633fbb73542..d2a04a7d21d11 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml @@ -9,28 +9,26 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryCatalogUiUsedInProductFilterTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1795"/> + </skip> <features value="AdminMediaGalleryUsedInProductsFilter"/> - <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> - <title value="Used in products filter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> + <title value="User can open product entity the asset is associated"/> <stories value="Story 58: User sees entities where asset is used in" /> - <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951848"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> <description value="User filters assets used in products"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> </annotations> <before> <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> - <createData entity="SimpleSubCategory" stepKey="category"/> - <createData entity="SimpleProduct" stepKey="product"> - <requiredEntity createDataKey="category"/> - </createData> + <createData entity="SimpleProduct2" stepKey="product"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> - <deleteData createDataKey="product" stepKey="deleteProduct"/> - <deleteData createDataKey="category" stepKey="deleteCategory"/> </after> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> <argument name="product" value="$$product$$"/> @@ -38,11 +36,7 @@ <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> <argument name="product" value="$$product$$"/> </actionGroup> - <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab"/> - <waitForElementVisible selector="{{CatalogWYSIWYGSection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> - <click selector="{{CatalogWYSIWYGSection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> - <waitForPageLoad stepKey="waitForPageLoad" /> - <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> <argument name="image" value="ImageUpload3"/> </actionGroup> @@ -59,17 +53,22 @@ <argument name="optionName" value="$$product.name$$"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> - <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> - <argument name="title" value="ImageMetadata.title"/> + + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInProducts"> + <argument name="entityName" value="Products"/> </actionGroup> - <wait time="10" stepKey="waitForBookmarkToSaveView"/> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForGridReloaded"/> - <actionGroup ref="AdminAssertMediaGalleryFilterPlaceholderActionGroup" stepKey="assertFilterApplied"> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> <argument name="filterPlaceholder" value="$$product.name$$"/> </actionGroup> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToAssertEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> <argument name="imageName" value="{{ImageMetadata.title}}"/> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml index 6b7bd3ba11f45..fde9597155d0c 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml @@ -26,6 +26,17 @@ <deleteData createDataKey="category" stepKey="deleteCategory"/> </after> <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> - <actionGroup ref="AdminAssertCategoryGridPageDetailsActionGroup" stepKey="assertCategoryGridPageRendered"/> + <actionGroup ref="AdminSearchCategoryGridPageByCategoryNameActionGroup" stepKey="searchByCategoryName"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageNumberOfRecordsActionGroup" stepKey="assertOneRecordInGrid"> + <argument name="numberOfRecords" value="1 records found"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageImageColumnActionGroup" stepKey="assertCategoryGridPageImageColumn"/> + <actionGroup ref="AssertAdminCategoryGridPageDetailsActionGroup" stepKey="assertCategoryGridPageRendered"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup" stepKey="assertCategoryGridPageProductsInMenuEnabledColumns"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> </test> </tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml index 7e0fa6c477c45..8191d5570f1e8 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1794"/> + </skip> <features value="AdminMediaGalleryCategoryGrid"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> <title value="User can open each entity the asset is associated with in a separate tab to manage association"/> @@ -23,14 +26,12 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> - <deleteData createDataKey="category" stepKey="deleteCategory"/> </after> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> @@ -57,8 +58,35 @@ <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterApplied"> <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> </actionGroup> - <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> - <argument name="categoryName" value="$$category.name$$"/> + <actionGroup ref="AssertAdminCategoryGridPageNumberOfRecordsActionGroup" stepKey="assertOneRecordInGrid"> + <argument name="numberOfRecords" value="1 records found"/> </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageImageColumnActionGroup" stepKey="assertCategoryGridPageImageColumn"> + <argument name="file" value="{{UpdatedImageDetails.file}}"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageDetailsActionGroup" stepKey="assertCategoryInGrid"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup" stepKey="assertCategoryGridPageProductsInMenuEnabledColumns"/> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="firstResetAdminDataGridToDefaultView"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setAssetFilter"> + <argument name="filterName" value="Asset"/> + <argument name="optionName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterAppliedAfterUrlFilterApplier"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerfifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + </test> </tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkProductGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkProductGridTest.xml new file mode 100644 index 0000000000000..db7942d4c53bf --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkProductGridTest.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="AdminMediaGalleryCatalogUiVerifyUsedInLinkProductGridTest"> + <annotations> + <features value="AdminMediaGalleryUsedInProductsFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in products filter"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951848"/> + <description value="User filters assets used in products"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab"/> + <waitForElementVisible selector="{{CatalogWYSIWYGSection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{CatalogWYSIWYGSection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInProducts"> + <argument name="entityName" value="Products"/> + </actionGroup> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> + </actionGroup> + + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerfifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php deleted file mode 100644 index d86617e12b8f8..0000000000000 --- a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php +++ /dev/null @@ -1,115 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Filters; - -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Data\OptionSourceInterface; -use Magento\Framework\View\Element\UiComponent\ContextInterface; -use Magento\Framework\View\Element\UiComponentFactory; -use Magento\Ui\Component\Filters\FilterModifier; -use Magento\Ui\Component\Filters\Type\Select; -use Magento\Ui\Api\BookmarkManagementInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; - -/** - * Used in products filter - */ -class UsedInProducts extends Select -{ - /** - * @var BookmarkManagementInterface - */ - private $bookmarkManagement; - - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - - /** - * Constructor - * - * @param ContextInterface $context - * @param UiComponentFactory $uiComponentFactory - * @param FilterBuilder $filterBuilder - * @param FilterModifier $filterModifier - * @param OptionSourceInterface $optionsProvider - * @param BookmarkManagementInterface $bookmarkManagement - * @param ProductRepositoryInterface $productRepository - * @param array $components - * @param array $data - */ - public function __construct( - ContextInterface $context, - UiComponentFactory $uiComponentFactory, - FilterBuilder $filterBuilder, - FilterModifier $filterModifier, - OptionSourceInterface $optionsProvider = null, - BookmarkManagementInterface $bookmarkManagement, - ProductRepositoryInterface $productRepository, - array $components = [], - array $data = [] - ) { - $this->uiComponentFactory = $uiComponentFactory; - $this->filterBuilder = $filterBuilder; - parent::__construct( - $context, - $uiComponentFactory, - $filterBuilder, - $filterModifier, - $optionsProvider, - $components, - $data - ); - $this->bookmarkManagement = $bookmarkManagement; - $this->productRepository = $productRepository; - } - - /** - * Prepare component configuration - * - * @return void - */ - public function prepare() - { - $options = []; - $productIds = []; - $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( - 'current', - $this->context->getNameSpace() - ); - if ($bookmark === null) { - parent::prepare(); - return; - } - - $applied = $bookmark->getConfig()['current']['filters']['applied']; - - if (isset($applied[$this->getName()])) { - $productIds = $applied[$this->getName()]; - } - - foreach ($productIds as $id) { - try { - $product = $this->productRepository->getById($id); - $options[] = [ - 'value' => $id, - 'label' => $product->getName(), - 'is_active' => $product->getStatus(), - 'path' => $product->getSku(), - 'optgroup' => false - ]; - } catch (\Exception $e) { - continue; - } - } - $this->optionsProvider = $options; - parent::prepare(); - } -} diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml index e12d90b95303b..17fe33e5b2bf5 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml @@ -58,7 +58,7 @@ provider="${ $.parentName }" sortOrder="10" class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -74,6 +74,7 @@ <item name="filterRateLimitMethod" xsi:type="string" translate="true">notifyWhenChangesStop</item> <item name="searchOptions" xsi:type="boolean">true</item> <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="validationUrl" xsi:type="url" path="media_gallery/asset/getSelected"/> <item name="levelsVisibility" xsi:type="number">1</item> </item> </argument> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml index 2ca58b6020fa7..6976584c2e36c 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -13,8 +13,7 @@ name="product_id" provider="${ $.parentName }" sortOrder="110" - class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Filters\UsedInProducts" - component="Magento_Catalog/js/components/product-ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -30,7 +29,7 @@ <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> <item name="levelsVisibility" xsi:type="number">1</item> <item name="searchUrl" xsi:type="url" path="catalog/product/search"/> - <item name="validationUrl" xsi:type="url" path="catalog/product/getSelected"/> + <item name="validationUrl" xsi:type="url" path="media_gallery_catalog/product/getSelected"/> </item> </argument> <settings> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml index 2ca58b6020fa7..6976584c2e36c 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -13,8 +13,7 @@ name="product_id" provider="${ $.parentName }" sortOrder="110" - class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Filters\UsedInProducts" - component="Magento_Catalog/js/components/product-ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -30,7 +29,7 @@ <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> <item name="levelsVisibility" xsi:type="number">1</item> <item name="searchUrl" xsi:type="url" path="catalog/product/search"/> - <item name="validationUrl" xsi:type="url" path="catalog/product/getSelected"/> + <item name="validationUrl" xsi:type="url" path="media_gallery_catalog/product/getSelected"/> </item> </argument> <settings> diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php new file mode 100644 index 0000000000000..a686f0e7b3ace --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Controller\Adminhtml\Block; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to get selected block for ui-select component + */ +class GetSelected extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::block'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @param JsonFactory $resultFactory + * @param BlockRepositoryInterface $blockRepository + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + BlockRepositoryInterface $blockRepository, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->blockRepository = $blockRepository; + parent::__construct($context); + } + + /** + * Return selected blocks options. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $options = []; + $blockIds = $this->getRequest()->getParam('ids'); + + if (!is_array($blockIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + } + foreach ($blockIds as $id) { + try { + $block = $this->blockRepository->getById($id); + $options[] = [ + 'value' => $block->getId(), + 'label' => $block->getTitle(), + 'is_active' => $block->isActive(), + 'optgroup' => false + ]; + } catch (\Exception $e) { + continue; + } + } + + return $this->resultJsonFactory->create()->setData($options); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php new file mode 100644 index 0000000000000..be6eb9fd9de9f --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Controller\Adminhtml\Page; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to get selected page for ui-select component + */ +class GetSelected extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::page'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @param JsonFactory $resultFactory + * @param PageRepositoryInterface $pageRepository + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + PageRepositoryInterface $pageRepository, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->pageRepository = $pageRepository; + parent::__construct($context); + } + + /** + * Return selected pages options. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $options = []; + $pageIds = $this->getRequest()->getParam('ids'); + + if (!is_array($pageIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + } + foreach ($pageIds as $id) { + try { + $page = $this->pageRepository->getById($id); + $options[] = [ + 'value' => $page->getId(), + 'label' => $page->getTitle(), + 'is_active' => $page->isActive(), + 'optgroup' => false + ]; + } catch (\Exception $e) { + continue; + } + } + + return $this->resultJsonFactory->create()->setData($options); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml new file mode 100644 index 0000000000000..a0cd04fad54c5 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.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="AdminMediaGalleryAssertUsedInLinkBlocksGridTest"> + <annotations> + <features value="AdminMediaGalleryUsedInBlocksFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in blocks link"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951848"/> + <description value="User filters assets used in blocks"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="_defaultBlock" stepKey="block" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </after> + + <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$block$$"/> + </actionGroup> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="saveBlock"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInPages"> + <argument name="entityName" value="Blocks"/> + </actionGroup> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> + </actionGroup> + + <deleteData createDataKey="block" stepKey="deleteBlock"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerfifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml new file mode 100644 index 0000000000000..5a375d9153a6d --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.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="AdminMediaGalleryAssertUsedInLinkPagesGridTest"> + <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> + <features value="AdminMediaGalleryUsedInBlocksFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in pages link"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951848"/> + <description value="User filters assets used in pages"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToCreateNewPage"/> + <actionGroup ref="FillOutCustomCMSPageContentActionGroup" stepKey="fillBasicPageDataForPageWithDefaultStore"> + <argument name="title" value="Unique page title MediaGalleryUi"/> + <argument name="content" value="MediaGalleryUI content"/> + <argument name="identifier" value="test-page-1"/> + </actionGroup> + + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="savePage"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInPages"> + <argument name="entityName" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerfifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml index 810d9eea4e261..fa6dc6c1a07fa 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml @@ -55,6 +55,6 @@ </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> - + </test> </tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml index a6bfdb781a734..e72e65cf8de90 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryCmsUiUsedInPagesFilterTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> <features value="AdminMediaGalleryUsedInPagesFilter"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> <title value="Used in pages filter"/> @@ -21,14 +24,14 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> - + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToCreateNewPage"/> <actionGroup ref="FillOutCustomCMSPageContentActionGroup" stepKey="fillBasicPageDataForPageWithDefaultStore"> <argument name="title" value="Unique page title MediaGalleryUi"/> <argument name="content" value="MediaGalleryUI content"/> <argument name="identifier" value="test-page-1"/> </actionGroup> - + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> <waitForPageLoad stepKey="waitForPageLoad" /> <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> @@ -39,7 +42,7 @@ </actionGroup> <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="savePage"/> - + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> @@ -56,8 +59,9 @@ <argument name="imageName" value="{{ImageMetadata.title}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> - + <actionGroup ref="AdminNavigateToPageGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> <actionGroup ref="AdminSearchCmsPageInGridByUrlKeyActionGroup" stepKey="findCreatedCmsPage"> <argument name="urlKey" value="test-page-1"/> </actionGroup> diff --git a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php deleted file mode 100644 index 66f8caa71d70a..0000000000000 --- a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php +++ /dev/null @@ -1,115 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters; - -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Data\OptionSourceInterface; -use Magento\Framework\View\Element\UiComponent\ContextInterface; -use Magento\Framework\View\Element\UiComponentFactory; -use Magento\Ui\Component\Filters\FilterModifier; -use Magento\Ui\Component\Filters\Type\Select; -use Magento\Ui\Api\BookmarkManagementInterface; -use Magento\Cms\Api\BlockRepositoryInterface; - -/** - * Used in blocks filter - */ -class UsedInBlocks extends Select -{ - /** - * @var BookmarkManagementInterface - */ - private $bookmarkManagement; - - /** - * @var BlockRepositoryInterface - */ - private $blockRepository; - - /** - * Constructor - * - * @param ContextInterface $context - * @param UiComponentFactory $uiComponentFactory - * @param FilterBuilder $filterBuilder - * @param FilterModifier $filterModifier - * @param OptionSourceInterface $optionsProvider - * @param BookmarkManagementInterface $bookmarkManagement - * @param BlockRepositoryInterface $blockRepository - * @param array $components - * @param array $data - */ - public function __construct( - ContextInterface $context, - UiComponentFactory $uiComponentFactory, - FilterBuilder $filterBuilder, - FilterModifier $filterModifier, - OptionSourceInterface $optionsProvider = null, - BookmarkManagementInterface $bookmarkManagement, - BlockRepositoryInterface $blockRepository, - array $components = [], - array $data = [] - ) { - $this->uiComponentFactory = $uiComponentFactory; - $this->filterBuilder = $filterBuilder; - parent::__construct( - $context, - $uiComponentFactory, - $filterBuilder, - $filterModifier, - $optionsProvider, - $components, - $data - ); - $this->bookmarkManagement = $bookmarkManagement; - $this->blockRepository = $blockRepository; - } - - /** - * Prepare component configuration - * - * @return void - */ - public function prepare() - { - $options = []; - $blockIds = []; - $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( - 'current', - $this->context->getNameSpace() - ); - if ($bookmark === null) { - parent::prepare(); - return; - } - - $applied = $bookmark->getConfig()['current']['filters']['applied']; - - if (isset($applied[$this->getName()])) { - $blockIds = $applied[$this->getName()]; - } - - foreach ($blockIds as $id) { - try { - $block = $this->blockRepository->getById($id); - $options[] = [ - 'value' => $id, - 'label' => $block->getTitle(), - 'is_active' => $block->isActive(), - 'optgroup' => false - ]; - } catch (\Exception $e) { - continue; - } - } - - $this->optionsProvider = $options; - parent::prepare(); - } -} diff --git a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php deleted file mode 100644 index 78ab1b63d32d1..0000000000000 --- a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters; - -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Data\OptionSourceInterface; -use Magento\Framework\View\Element\UiComponent\ContextInterface; -use Magento\Framework\View\Element\UiComponentFactory; -use Magento\Ui\Component\Filters\FilterModifier; -use Magento\Ui\Component\Filters\Type\Select; -use Magento\Ui\Api\BookmarkManagementInterface; -use Magento\Cms\Api\PageRepositoryInterface; - -/** - * Used in pages filter - */ -class UsedInPages extends Select -{ - /** - * @var BookmarkManagementInterface - */ - private $bookmarkManagement; - - /** - * @var PageRepositoryInterface - */ - private $pageRepository; - - /** - * Constructor - * - * @param ContextInterface $context - * @param UiComponentFactory $uiComponentFactory - * @param FilterBuilder $filterBuilder - * @param FilterModifier $filterModifier - * @param OptionSourceInterface $optionsProvider - * @param BookmarkManagementInterface $bookmarkManagement - * @param PageRepositoryInterface $pageRepository - * @param array $components - * @param array $data - */ - public function __construct( - ContextInterface $context, - UiComponentFactory $uiComponentFactory, - FilterBuilder $filterBuilder, - FilterModifier $filterModifier, - OptionSourceInterface $optionsProvider = null, - BookmarkManagementInterface $bookmarkManagement, - PageRepositoryInterface $pageRepository, - array $components = [], - array $data = [] - ) { - $this->uiComponentFactory = $uiComponentFactory; - $this->filterBuilder = $filterBuilder; - parent::__construct( - $context, - $uiComponentFactory, - $filterBuilder, - $filterModifier, - $optionsProvider, - $components, - $data - ); - $this->bookmarkManagement = $bookmarkManagement; - $this->pageRepository = $pageRepository; - } - - /** - * Prepare component configuration - * - * @return void - */ - public function prepare() - { - $options = []; - $pageIds = []; - $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( - 'current', - $this->context->getNameSpace() - ); - if ($bookmark === null) { - parent::prepare(); - return; - } - - $applied = $bookmark->getConfig()['current']['filters']['applied']; - - if (isset($applied[$this->getName()])) { - $pageIds = $applied[$this->getName()]; - } - - foreach ($pageIds as $id) { - try { - $page = $this->pageRepository->getById($id); - $options[] = [ - 'value' => $id, - 'label' => $page->getTitle(), - 'is_active' => $page->isActive(), - 'optgroup' => false - ]; - } catch (\Exception $e) { - continue; - } - } - $this->optionsProvider = $options; - parent::prepare(); - } -} diff --git a/app/code/Magento/MediaGalleryCmsUi/composer.json b/app/code/Magento/MediaGalleryCmsUi/composer.json index 73747a669c051..1ecfb9a3c8855 100644 --- a/app/code/Magento/MediaGalleryCmsUi/composer.json +++ b/app/code/Magento/MediaGalleryCmsUi/composer.json @@ -5,8 +5,7 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-cms": "*", - "magento/module-backend": "*", - "magento/module-ui": "*" + "magento/module-backend": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml index 506a6cad5b68e..e49ba7a98c8ce 100644 --- a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -13,8 +13,7 @@ name="page_id" provider="${ $.parentName }" sortOrder="120" - class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInPages" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -27,6 +26,7 @@ <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find pages</item> <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="validationUrl" xsi:type="url" path="media_gallery_cms/page/getSelected"/> </item> </argument> <settings> @@ -38,8 +38,7 @@ name="block_id" provider="${ $.parentName }" sortOrder="130" - class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInBlocks" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -52,6 +51,7 @@ <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find blocks</item> <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="validationUrl" xsi:type="url" path="media_gallery_cms/block/getSelected"/> </item> </argument> <settings> diff --git a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml index 506a6cad5b68e..e49ba7a98c8ce 100644 --- a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -13,8 +13,7 @@ name="page_id" provider="${ $.parentName }" sortOrder="120" - class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInPages" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -27,6 +26,7 @@ <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find pages</item> <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="validationUrl" xsi:type="url" path="media_gallery_cms/page/getSelected"/> </item> </argument> <settings> @@ -38,8 +38,7 @@ name="block_id" provider="${ $.parentName }" sortOrder="130" - class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInBlocks" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -52,6 +51,7 @@ <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find blocks</item> <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="validationUrl" xsi:type="url" path="media_gallery_cms/block/getSelected"/> </item> </argument> <settings> diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php index 00f2b07f5bb81..f5efd25bca041 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php @@ -7,7 +7,6 @@ namespace Magento\MediaGalleryMetadata\Model\File; -use Magento\Framework\Exception\LocalizedException; use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; @@ -90,7 +89,12 @@ private function readSegments(FileInterface $file): MetadataInterface ); } - $data = $segmentReader->execute($file); + try { + $data = $segmentReader->execute($file); + } catch (\Exception $exception) { + continue; + } + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; $description = !empty($data->getDescription()) ? $data->getDescription() : $description; diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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. diff --git a/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditions/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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/MediaGalleryRenditions/Model/Config.php b/app/code/Magento/MediaGalleryRenditions/Model/Config.php new file mode 100644 index 0000000000000..d1a48904d1f13 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Config.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Class responsible for providing access to Media Gallery Renditions system configuration. + */ +class Config +{ + private const TABLE_CORE_CONFIG_DATA = 'core_config_data'; + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + ResourceConnection $resourceConnection + ) { + $this->scopeConfig = $scopeConfig; + $this->resourceConnection = $resourceConnection; + } + + /** + * Check if the media gallery is enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } + + /** + * Get max width + * + * @return int + */ + public function getWidth(): int + { + try { + return $this->getDatabaseValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); + } catch (NoSuchEntityException $exception) { + return (int) $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH); + } + } + + /** + * Get max height + * + * @return int + */ + public function getHeight(): int + { + try { + return $this->getDatabaseValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); + } catch (NoSuchEntityException $exception) { + return (int) $this->scopeConfig->getValue(self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH); + } + } + + /** + * Get value from database bypassing config cache + * + * @param string $path + * @return int + * @throws NoSuchEntityException + */ + private function getDatabaseValue(string $path): int + { + $connection = $this->resourceConnection->getConnection(); + $select = $connection->select() + ->from( + [ + 'config' => $this->resourceConnection->getTableName(self::TABLE_CORE_CONFIG_DATA) + ], + [ + 'value' + ] + ) + ->where('config.path = ?', $path); + $value = $connection->query($select)->fetchColumn(); + + if ($value === false) { + throw new NoSuchEntityException( + __( + 'The config value for %path is not saved to database.', + ['path' => $path] + ) + ); + } + + return (int) $value; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php new file mode 100644 index 0000000000000..6bc54fdf9aca4 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/GenerateRenditions.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Image\AdapterFactory; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +class GenerateRenditions implements GenerateRenditionsInterface +{ + private const IMAGE_FILE_NAME_PATTERN = '#\.(jpg|jpeg|gif|png)$# i'; + + /** + * @var AdapterFactory + */ + private $imageFactory; + + /** + * @var Config + */ + private $config; + + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var File + */ + private $driver; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param AdapterFactory $imageFactory + * @param Config $config + * @param GetRenditionPathInterface $getRenditionPath + * @param Filesystem $filesystem + * @param File $driver + * @param IsPathExcludedInterface $isPathExcluded + * @param LoggerInterface $log + */ + public function __construct( + AdapterFactory $imageFactory, + Config $config, + GetRenditionPathInterface $getRenditionPath, + Filesystem $filesystem, + File $driver, + IsPathExcludedInterface $isPathExcluded, + LoggerInterface $log + ) { + $this->imageFactory = $imageFactory; + $this->config = $config; + $this->getRenditionPath = $getRenditionPath; + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->isPathExcluded = $isPathExcluded; + $this->log = $log; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $failedPaths = []; + + foreach ($paths as $path) { + try { + $this->generateRendition($path); + } catch (\Exception $exception) { + $this->log->error($exception); + $failedPaths[] = $path; + } + } + + if (!empty($failedPaths)) { + throw new LocalizedException( + __( + 'Cannot create rendition for media asset paths: %paths', + [ + 'paths' => implode(', ', $failedPaths) + ] + ) + ); + } + } + + /** + * Generate rendition for media asset path + * + * @param string $path + * @throws FileSystemException + * @throws LocalizedException + * @throws \Exception + */ + private function generateRendition(string $path): void + { + $this->validateAsset($path); + + $renditionPath = $this->getRenditionPath->execute($path); + $this->createDirectory($renditionPath); + + $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); + + if ($this->shouldFileBeResized($absolutePath)) { + $this->createResizedRendition( + $absolutePath, + $this->getMediaDirectory()->getAbsolutePath($renditionPath) + ); + } else { + $this->getMediaDirectory()->copyFile($path, $renditionPath); + } + } + + /** + * Ensure valid media asset path is provided for renditions generation + * + * @param string $path + * @throws FileSystemException + * @throws LocalizedException + */ + private function validateAsset(string $path): void + { + if (!$this->getMediaDirectory()->isFile($path)) { + throw new LocalizedException(__('Media asset file %path does not exist!', ['path' => $path])); + } + + if ($this->isPathExcluded->execute($path)) { + throw new LocalizedException( + __('Could not create rendition for image, path is restricted: %path', ['path' => $path]) + ); + } + + if (!preg_match(self::IMAGE_FILE_NAME_PATTERN, $path)) { + throw new LocalizedException( + __('Could not create rendition for image, unsupported file type: %path.', ['path' => $path]) + ); + } + } + + /** + * Create directory for rendition file + * + * @param string $path + * @throws LocalizedException + */ + private function createDirectory(string $path): void + { + try { + $this->getMediaDirectory()->create($this->driver->getParentDirectory($path)); + } catch (\Exception $exception) { + throw new LocalizedException(__('Cannot create directory for rendition %path', ['path' => $path])); + } + } + + /** + * Create rendition file + * + * @param string $absolutePath + * @param string $absoluteRenditionPath + * @throws \Exception + */ + private function createResizedRendition(string $absolutePath, string $absoluteRenditionPath): void + { + $image = $this->imageFactory->create(); + $image->open($absolutePath); + $image->keepAspectRatio(true); + $image->resize($this->config->getWidth(), $this->config->getHeight()); + $image->save($absoluteRenditionPath); + } + + /** + * Check if image needs to resize or not + * + * @param string $absolutePath + * @return bool + */ + private function shouldFileBeResized(string $absolutePath): bool + { + [$width, $height] = getimagesize($absolutePath); + return $width > $this->config->getWidth() || $height > $this->config->getHeight(); + } + + /** + * Retrieve a media directory instance with write permissions + * + * @return WriteInterface + * @throws FileSystemException + */ + private function getMediaDirectory(): WriteInterface + { + return $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php b/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php new file mode 100644 index 0000000000000..1c93141429ab0 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/GetRenditionPath.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model; + +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; + +class GetRenditionPath implements GetRenditionPathInterface +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * Returns Rendition image path + * + * @param string $path + * @return string + */ + public function execute(string $path): string + { + return self::RENDITIONS_DIRECTORY_NAME . '/' . ltrim($path, '/'); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php new file mode 100644 index 0000000000000..7263010a8f587 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/FetchRenditionPathsBatches.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Psr\Log\LoggerInterface; + +/** + * Fetch files from media storage in batches + */ +class FetchRenditionPathsBatches +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * @var GetFilesIterator + */ + private $getFilesIterator; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $fileExtensions; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var int + */ + private $batchSize; + + /** + * @param LoggerInterface $log + * @param Filesystem $filesystem + * @param GetFilesIterator $getFilesIterator + * @param int $batchSize + * @param array $fileExtensions + */ + public function __construct( + LoggerInterface $log, + Filesystem $filesystem, + GetFilesIterator $getFilesIterator, + int $batchSize, + array $fileExtensions + ) { + $this->log = $log; + $this->getFilesIterator = $getFilesIterator; + $this->filesystem = $filesystem; + $this->batchSize = $batchSize; + $this->fileExtensions = $fileExtensions; + } + + /** + * Return files from files system by provided size of batch + */ + public function execute(): \Traversable + { + $index = 0; + $batch = []; + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + $iterator = $this->getFilesIterator->execute( + $mediaDirectory->getAbsolutePath(self::RENDITIONS_DIRECTORY_NAME) + ); + + /** @var \SplFileInfo $file */ + foreach ($iterator as $file) { + $relativePath = $mediaDirectory->getRelativePath($file->getPathName()); + if (!$this->isApplicable($relativePath)) { + continue; + } + + $batch[] = $relativePath; + if (++$index == $this->batchSize) { + yield $batch; + $index = 0; + $batch = []; + } + } + if (count($batch) > 0) { + yield $batch; + } + } + + /** + * Is the path a valid image path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path && preg_match('#\.(' . implode("|", $this->fileExtensions) . ')$# i', $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php new file mode 100644 index 0000000000000..97efcdc81ba50 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/GetFilesIterator.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +/** + * Retrieve files iterator for path + */ +class GetFilesIterator +{ + /** + * Get files iterator for provided path + * + * @param string $path + * @return \RecursiveIteratorIterator + */ + public function execute(string $path): \RecursiveIteratorIterator + { + return new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $path, + \FilesystemIterator::SKIP_DOTS | + \FilesystemIterator::UNIX_PATHS | + \RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php new file mode 100644 index 0000000000000..051c883025587 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/ScheduleRenditionsUpdate.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\MessageQueue\PublisherInterface; + +/** + * Publish media gallery renditions update message to the queue. + */ +class ScheduleRenditionsUpdate +{ + private const TOPIC_MEDIA_GALLERY_UPDATE_RENDITIONS = 'media.gallery.renditions.update'; + + /** + * @var PublisherInterface + */ + private $publisher; + + /** + * @param PublisherInterface $publisher + */ + public function __construct(PublisherInterface $publisher) + { + $this->publisher = $publisher; + } + + /** + * Publish media gallery renditions update message to the queue. + * + * @param array $paths + */ + public function execute(array $paths = []): void + { + $this->publisher->publish( + self::TOPIC_MEDIA_GALLERY_UPDATE_RENDITIONS, + $paths + ); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php new file mode 100644 index 0000000000000..45cea58d05018 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Model/Queue/UpdateRenditions.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Model\Queue; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Psr\Log\LoggerInterface; + +/** + * Renditions update queue consumer. + */ +class UpdateRenditions +{ + private const RENDITIONS_DIRECTORY_NAME = '.renditions'; + + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var FetchRenditionPathsBatches + */ + private $fetchRenditionPathsBatches; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GenerateRenditionsInterface $generateRenditions + * @param FetchRenditionPathsBatches $fetchRenditionPathsBatches + * @param LoggerInterface $log + */ + public function __construct( + GenerateRenditionsInterface $generateRenditions, + FetchRenditionPathsBatches $fetchRenditionPathsBatches, + LoggerInterface $log + ) { + $this->generateRenditions = $generateRenditions; + $this->fetchRenditionPathsBatches = $fetchRenditionPathsBatches; + $this->log = $log; + } + + /** + * Update renditions for given paths, if empty array is provided - all renditions are updated + * + * @param array $paths + * @throws LocalizedException + */ + public function execute(array $paths): void + { + if (!empty($paths)) { + $this->updateRenditions($paths); + return; + } + + foreach ($this->fetchRenditionPathsBatches->execute() as $renditionPaths) { + $this->updateRenditions($renditionPaths); + } + } + + /** + * Update renditions and log exceptions + * + * @param string[] $renditionPaths + */ + private function updateRenditions(array $renditionPaths): void + { + try { + $this->generateRenditions->execute($this->getAssetPaths($renditionPaths)); + } catch (LocalizedException $exception) { + $this->log->error($exception); + } + } + + /** + * Get asset paths based on rendition paths + * + * @param string[] $renditionPaths + * @return string[] + */ + private function getAssetPaths(array $renditionPaths): array + { + $paths = []; + + foreach ($renditionPaths as $renditionPath) { + try { + $paths[] = $this->getAssetPath($renditionPath); + } catch (\Exception $exception) { + $this->log->error($exception); + } + } + + return $paths; + } + + /** + * Get asset path based on rendition path + * + * @param string $renditionPath + * @return string + * @throws LocalizedException + */ + private function getAssetPath(string $renditionPath): string + { + if (strpos($renditionPath, self::RENDITIONS_DIRECTORY_NAME) !== 0) { + throw new LocalizedException( + __( + 'Incorrect rendition path provided for update: %path', + [ + 'path' => $renditionPath + ] + ) + ); + } + + return substr($renditionPath, strlen(self::RENDITIONS_DIRECTORY_NAME)); + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php new file mode 100644 index 0000000000000..f0ba8c3533722 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/RemoveRenditions.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +/** + * Remove renditions when assets are removed + */ +class RemoveRenditions +{ + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GetRenditionPathInterface $getRenditionPath + * @param Filesystem $filesystem + * @param LoggerInterface $log + */ + public function __construct( + GetRenditionPathInterface $getRenditionPath, + Filesystem $filesystem, + LoggerInterface $log + ) { + $this->getRenditionPath = $getRenditionPath; + $this->filesystem = $filesystem; + $this->log = $log; + } + + /** + * Remove renditions when assets are removed + * + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param void $result + * @param array $paths + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute( + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + $result, + array $paths + ): void { + $this->removeRenditions($paths); + } + + /** + * Remove rendition files + * + * @param array $paths + */ + private function removeRenditions(array $paths): void + { + foreach ($paths as $path) { + try { + $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA)->delete( + $this->getRenditionPath->execute($path) + ); + } catch (\Exception $exception) { + $this->log->error($exception); + } + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php b/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php new file mode 100644 index 0000000000000..ec2012c528ef1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/SetRenditionPath.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\GetInsertImageContent; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryRenditions\Model\Config; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Psr\Log\LoggerInterface; + +/** + * Intercept and set renditions path on PrepareImage + */ +class SetRenditionPath +{ + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var Images + */ + private $imagesHelper; + + /** + * @var Config + */ + private $config; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @param GetRenditionPathInterface $getRenditionPath + * @param GenerateRenditionsInterface $generateRenditions + * @param Images $imagesHelper + * @param Config $config + * @param LoggerInterface $log + */ + public function __construct( + GetRenditionPathInterface $getRenditionPath, + GenerateRenditionsInterface $generateRenditions, + Images $imagesHelper, + Config $config, + LoggerInterface $log + ) { + $this->getRenditionPath = $getRenditionPath; + $this->generateRenditions = $generateRenditions; + $this->imagesHelper = $imagesHelper; + $this->config = $config; + $this->log = $log; + } + + /** + * Replace the original asset path with rendition path + * + * @param GetInsertImageContent $subject + * @param string $encodedFilename + * @param bool $forceStaticPath + * @param bool $renderAsTag + * @param int|null $storeId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute( + GetInsertImageContent $subject, + string $encodedFilename, + bool $forceStaticPath, + bool $renderAsTag, + ?int $storeId = null + ): array { + $arguments = [ + $encodedFilename, + $forceStaticPath, + $renderAsTag, + $storeId + ]; + + if (!$this->config->isEnabled()) { + return $arguments; + } + + $path = $this->imagesHelper->idDecode($encodedFilename); + + try { + $this->generateRenditions->execute([$path]); + } catch (LocalizedException $exception) { + $this->log->error($exception); + return $arguments; + } + + $arguments[0] = $this->imagesHelper->idEncode($this->getRenditionPath->execute($path)); + + return $arguments; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php new file mode 100644 index 0000000000000..9cf969c16782f --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Plugin/UpdateRenditionsOnConfigChange.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Plugin; + +use Magento\Framework\App\Config\Value; +use Magento\MediaGalleryRenditions\Model\Queue\ScheduleRenditionsUpdate; + +/** + * Update renditions if corresponding configuration changes + */ +class UpdateRenditionsOnConfigChange +{ + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH = 'system/media_gallery_renditions/width'; + private const XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH = 'system/media_gallery_renditions/height'; + + /** + * @var ScheduleRenditionsUpdate + */ + private $scheduleRenditionsUpdate; + + /** + * @param ScheduleRenditionsUpdate $scheduleRenditionsUpdate + */ + public function __construct(ScheduleRenditionsUpdate $scheduleRenditionsUpdate) + { + $this->scheduleRenditionsUpdate = $scheduleRenditionsUpdate; + } + + /** + * Update renditions when configuration is changed + * + * @param Value $config + * @param Value $result + * @return Value + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Value $config, Value $result): Value + { + if ($this->isRenditionsValue($result) && $result->isValueChanged()) { + $this->scheduleRenditionsUpdate->execute(); + } + + return $result; + } + + /** + * Does configuration value relates to renditions + * + * @param Value $value + * @return bool + */ + private function isRenditionsValue(Value $value): bool + { + return $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_WIDTH_PATH + || $value->getPath() === self::XML_PATH_MEDIA_GALLERY_RENDITIONS_HEIGHT_PATH; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/README.md b/app/code/Magento/MediaGalleryRenditions/README.md new file mode 100644 index 0000000000000..df856e8003a84 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditions module + +The Magento_MediaGalleryRenditions module implements height and width fields for for media gallery items. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php new file mode 100644 index 0000000000000..05bb01b9ff433 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/ExtractAssetsFromContentWithRenditionTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for Extracting assets from rendition paths/urls in content + */ +class ExtractAssetsFromContentWithRenditionTest extends TestCase +{ + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->extractAssetsFromContent = Bootstrap::getObjectManager() + ->get(ExtractAssetsFromContentInterface::class); + } + + /** + * Assert rendition urls/path in the content are associated with an asset + * + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * + * @dataProvider contentProvider + * @param string $content + * @param array $assetIds + */ + public function testExecute(string $content, array $assetIds): void + { + $assets = $this->extractAssetsFromContent->execute($content); + + $extractedAssetIds = []; + foreach ($assets as $asset) { + $extractedAssetIds[] = $asset->getId(); + } + + sort($assetIds); + sort($extractedAssetIds); + + $this->assertEquals($assetIds, $extractedAssetIds); + } + + /** + * Data provider for testExecute + * + * @return array + */ + public function contentProvider() + { + return [ + 'Empty Content' => [ + '', + [] + ], + 'No paths in content' => [ + 'content without paths', + [] + ], + 'Relevant rendition path in content' => [ + 'content {{media url=".renditions/testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant wysiwyg rendition path in content' => [ + 'content <img src="https://domain.com/media/.renditions/testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant rendition path content with pub' => [ + '/pub/media/.renditions/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Relevant rendition path content' => [ + '/media/.renditions/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Relevant existing media paths w/o rendition in content' => [ + 'content {{media url="testDirectory/path.jpg"}} content', + [ + 2020 + ] + ], + 'Relevant existing paths w/o rendition in content with pub' => [ + '/pub/media/testDirectory/path.jpg', + [ + 2020 + ] + ], + 'Non-existing rendition paths in content' => [ + 'content {{media url=".renditions/non-existing-path.png"}} content', + [] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php new file mode 100644 index 0000000000000..9655f3949d404 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GenerateRenditionsTest.php @@ -0,0 +1,124 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\MediaGalleryRenditions\Model\Config; +use PHPUnit\Framework\TestCase; + +class GenerateRenditionsTest extends TestCase +{ + /** + * @var GenerateRenditionsInterface + */ + private $generateRenditions; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var Config + */ + private $renditionSizeConfig; + + /** + * @var DriverInterface + */ + private $driver; + + protected function setup(): void + { + $this->generateRenditions = Bootstrap::getObjectManager()->get(GenerateRenditionsInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->renditionSizeConfig = Bootstrap::getObjectManager()->get(Config::class); + } + + public static function tearDownAfterClass(): void + { + /** @var WriteInterface $mediaDirectory */ + $mediaDirectory = Bootstrap::getObjectManager()->get( + Filesystem::class + )->getDirectoryWrite( + DirectoryList::MEDIA + ); + if ($mediaDirectory->isExist($mediaDirectory->getAbsolutePath() . '/.renditions')) { + $mediaDirectory->delete($mediaDirectory->getAbsolutePath() . '/.renditions'); + } + } + + /** + * @dataProvider renditionsImageProvider + * + * Test for generation of rendition images. + * + * @param string $path + * @param string $renditionPath + * @throws LocalizedException + */ + public function testExecute(string $path, string $renditionPath): void + { + $this->copyImage($path); + $this->generateRenditions->execute([$path]); + $expectedRenditionPath = $this->mediaDirectory->getAbsolutePath($renditionPath); + list($imageWidth, $imageHeight) = getimagesize($expectedRenditionPath); + $this->assertFileExists($expectedRenditionPath); + $this->assertLessThanOrEqual( + $this->renditionSizeConfig->getWidth(), + $imageWidth, + 'Generated renditions image width should be less than or equal to configured value' + ); + $this->assertLessThanOrEqual( + $this->renditionSizeConfig->getHeight(), + $imageHeight, + 'Generated renditions image height should be less than or equal to configured value' + ); + } + + /** + * @param array $paths + * @throws FileSystemException + */ + private function copyImage(string $path): void + { + $imagePath = realpath(__DIR__ . '/../../_files' . $path); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($path); + $this->driver->copy( + $imagePath, + $modifiableFilePath + ); + } + + /** + * @return array + */ + public function renditionsImageProvider(): array + { + return [ + 'rendition_image_not_generated' => [ + 'paths' => '/magento_medium_image.jpg', + 'renditionPath' => ".renditions/magento_medium_image.jpg" + ], + 'rendition_image_generated' => [ + 'paths' => '/magento_large_image.jpg', + 'renditionPath' => ".renditions/magento_large_image.jpg" + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php new file mode 100644 index 0000000000000..0f8b61147986c --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Integration/Model/GetRenditionPathTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditions\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class GetRenditionPathTest extends TestCase +{ + + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var DriverInterface + */ + private $driver; + + protected function setup(): void + { + $this->getRenditionPath = Bootstrap::getObjectManager()->get(GetRenditionPathInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + } + + /** + * @dataProvider getImageProvider + * + * Test for getting a rendition path. + */ + public function testExecute(string $path, string $expectedRenditionPath): void + { + $imagePath = realpath(__DIR__ . '/../../_files' . $path); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($path); + $this->driver->copy( + $imagePath, + $modifiableFilePath + ); + $this->assertEquals($expectedRenditionPath, $this->getRenditionPath->execute($path)); + } + + /** + * @return array + */ + public function getImageProvider(): array + { + return [ + 'return_original_path' => [ + 'path' => '/magento_medium_image.jpg', + 'expectedRenditionPath' => '.renditions/magento_medium_image.jpg' + ], + 'return_rendition_path' => [ + 'path' => '/magento_large_image.jpg', + 'expectedRenditionPath' => '.renditions/magento_large_image.jpg' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg new file mode 100644 index 0000000000000..c377daf8fb0b3 Binary files /dev/null and b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_large_image.jpg differ diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_metadata.jpg b/app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg similarity index 100% rename from app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_metadata.jpg rename to app/code/Magento/MediaGalleryRenditions/Test/_files/magento_medium_image.jpg diff --git a/app/code/Magento/MediaGalleryRenditions/composer.json b/app/code/Magento/MediaGalleryRenditions/composer.json new file mode 100644 index 0000000000000..873e0b4a8c60b --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-media-gallery-renditions", + "description": "Magento module that implements height and width fields for for media gallery items.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-renditions-api": "*", + "magento/module-media-gallery-api": "*", + "magento/framework-message-queue": "*", + "magento/module-cms": "*" + }, + "suggest": { + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditions\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..64f338d53a283 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/adminhtml/system.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:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_gallery_renditions" translate="label" type="text" sortOrder="1010" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Media Gallery Renditions</label> + <field id="width" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Max Width</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + <field id="height" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Max Height</label> + <validate>validate-zero-or-greater validate-digits</validate> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/communication.xml b/app/code/Magento/MediaGalleryRenditions/etc/communication.xml new file mode 100644 index 0000000000000..2c343c4f8086a --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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="media.gallery.renditions.update" is_synchronous="false" request="string[]"> + <handler name="media.gallery.renditions.update.handler" + type="Magento\MediaGalleryRenditions\Model\Queue\UpdateRenditions" method="execute"/> + </topic> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml new file mode 100644 index 0000000000000..58c5aa1f11fd2 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/config.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:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <media_gallery_renditions> + <width>1000</width> + <height>1000</height> + </media_gallery_renditions> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/di.xml b/app/code/Magento/MediaGalleryRenditions/etc/di.xml new file mode 100644 index 0000000000000..af53810b7f69e --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/di.xml @@ -0,0 +1,31 @@ +<?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\MediaGalleryRenditionsApi\Api\GenerateRenditionsInterface" type="Magento\MediaGalleryRenditions\Model\GenerateRenditions"/> + <preference for="Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface" type="Magento\MediaGalleryRenditions\Model\GetRenditionPath"/> + <type name="Magento\Cms\Model\Wysiwyg\Images\GetInsertImageContent"> + <plugin name="set_rendition_path" type="Magento\MediaGalleryRenditions\Plugin\SetRenditionPath"/> + </type> + <type name="Magento\MediaGalleryRenditions\Model\Queue\FetchRenditionPathsBatches"> + <arguments> + <argument name="batchSize" xsi:type="number">100</argument> + <argument name="fileExtensions" xsi:type="array"> + <item name="jpg" xsi:type="string">jpg</item> + <item name="jpeg" xsi:type="string">jpeg</item> + <item name="gif" xsi:type="string">gif</item> + <item name="png" xsi:type="string">png</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\Config\Value"> + <plugin name="admin_system_config_media_gallery_renditions" type="Magento\MediaGalleryRenditions\Plugin\UpdateRenditionsOnConfigChange"/> + </type> + <type name="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface"> + <plugin name="delete_renditions_on_assets_delete" type="Magento\MediaGalleryRenditions\Plugin\RemoveRenditions"/> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml b/app/code/Magento/MediaGalleryRenditions/etc/media_content.xml new file mode 100644 index 0000000000000..a1fbe5cba558e --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/media_content.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_MediaContentApi:etc/media_content.xsd"> + <search> + <patterns> + <pattern name="media_gallery_renditions">/{{media url=(?:"|&quot;)(?:.renditions)?(.*?)(?:"|&quot;)}}/</pattern> + <pattern name="media_gallery">/{{media url="?((?!.*.renditions).*?)"?}}/</pattern> + <pattern name="wysiwyg">/src=".*\/media\/(?:.renditions\/)*(.*?)"/</pattern> + <pattern name="catalog_image">/^\/?media\/(?:.renditions\/)?(.*)/</pattern> + <pattern name="catalog_image_with_pub">/^\/pub\/?media\/(?:.renditions\/)?(.*)/</pattern> + </patterns> + </search> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/module.xml b/app/code/Magento/MediaGalleryRenditions/etc/module.xml new file mode 100644 index 0000000000000..93bc9f1c214e6 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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_MediaGalleryRenditions"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_consumer.xml new file mode 100644 index 0000000000000..0c584ac12f898 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/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="media.gallery.renditions.update" queue="media.gallery.renditions.update" + connection="db" handler="Magento\MediaGalleryRenditions\Model\Queue\UpdateRenditions::execute"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml new file mode 100644 index 0000000000000..9618329895230 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<!-- +/** + * 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="media.gallery.renditions.update"> + <connection name="db" exchange="magento-db" disabled="false" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml b/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.xml new file mode 100644 index 0000000000000..260e9f5f7f371 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/etc/queue_topology.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/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="MediaGalleryRenditions" topic="media.gallery.renditions.update" + destinationType="queue" destination="media.gallery.renditions.update"/> + </exchange> +</config> diff --git a/app/code/Magento/MediaGalleryRenditions/registration.php b/app/code/Magento/MediaGalleryRenditions/registration.php new file mode 100644 index 0000000000000..275c06f752a63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditions', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php b/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php new file mode 100644 index 0000000000000..b3ad5543c17fa --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/Api/GenerateRenditionsInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditionsApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Generate optimized version of media assets based on configuration for insertion to content + */ +interface GenerateRenditionsInterface +{ + /** + * Generate image renditions + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php b/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php new file mode 100644 index 0000000000000..b00c3615d9a29 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/Api/GetRenditionPathInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryRenditionsApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Based on media assset path provides path to an optimized image version for insertion to the content + */ +interface GetRenditionPathInterface +{ + /** + * Get Renditions image path + * + * @param string $path + * @return string + * @throws LocalizedException + */ + public function execute(string $path): string; +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/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. diff --git a/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryRenditionsApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/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/MediaGalleryRenditionsApi/README.md b/app/code/Magento/MediaGalleryRenditionsApi/README.md new file mode 100644 index 0000000000000..42478c0c9b520 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryRenditionsApi module + +The Magento_MediaGalleryRenditionsApi module is responsible for the API implementation of Media Gallery Renditions. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditionsApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryRenditionsApi/composer.json b/app/code/Magento/MediaGalleryRenditionsApi/composer.json new file mode 100644 index 0000000000000..6e3c559f001c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-renditions-api", + "description": "Magento module that is responsible for the API implementation of Media Gallery Renditions.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryRenditionsApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml b/app/code/Magento/MediaGalleryRenditionsApi/etc/module.xml new file mode 100644 index 0000000000000..f3a3f87b61105 --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/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_MediaGalleryRenditionsApi"/> +</config> diff --git a/app/code/Magento/MediaGalleryRenditionsApi/registration.php b/app/code/Magento/MediaGalleryRenditionsApi/registration.php new file mode 100644 index 0000000000000..bf057f2d2adbf --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditionsApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryRenditionsApi', + __DIR__ +); diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php index 79c0c9a1a803b..b796d4225d08c 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php @@ -7,6 +7,8 @@ namespace Magento\MediaGallerySynchronization\Model; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; /** @@ -19,19 +21,35 @@ class Consume */ private $synchronize; + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + /** * @param SynchronizeInterface $synchronize + * @param SynchronizeFilesInterface $synchronizeFiles */ - public function __construct(SynchronizeInterface $synchronize) - { + public function __construct( + SynchronizeInterface $synchronize, + SynchronizeFilesInterface $synchronizeFiles + ) { $this->synchronize = $synchronize; + $this->synchronizeFiles = $synchronizeFiles; } /** * Run media files synchronization. + * + * @param array $paths + * @throws LocalizedException */ - public function execute() : void + public function execute(array $paths) : void { - $this->synchronize->execute(); + if (!empty($paths)) { + $this->synchronizeFiles->execute($paths); + } else { + $this->synchronize->execute(); + } } } diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index 80b334733ed43..b4c360c3e0538 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -14,14 +14,13 @@ use Magento\Framework\Filesystem\Driver\File; use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; -use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; use Magento\MediaGallerySynchronization\Model\Filesystem\GetFileInfo; -use Magento\MediaGallerySynchronization\Model\GetContentHash; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface; /** * Create media asset object based on the file information */ -class CreateAssetFromFile +class CreateAssetFromFile implements CreateAssetFromFileInterface { /** * @var Filesystem @@ -43,11 +42,6 @@ class CreateAssetFromFile */ private $getContentHash; - /** - * @var ExtractMetadataInterface - */ - private $extractMetadata; - /** * @var GetFileInfo */ @@ -58,7 +52,6 @@ class CreateAssetFromFile * @param File $driver * @param AssetInterfaceFactory $assetFactory * @param GetContentHash $getContentHash - * @param ExtractMetadataInterface $extractMetadata * @param GetFileInfo $getFileInfo */ public function __construct( @@ -66,23 +59,17 @@ public function __construct( File $driver, AssetInterfaceFactory $assetFactory, GetContentHash $getContentHash, - ExtractMetadataInterface $extractMetadata, GetFileInfo $getFileInfo ) { $this->filesystem = $filesystem; $this->driver = $driver; $this->assetFactory = $assetFactory; $this->getContentHash = $getContentHash; - $this->extractMetadata = $extractMetadata; $this->getFileInfo = $getFileInfo; } /** - * Create and format media asset object - * - * @param string $path - * @return AssetInterface - * @throws FileSystemException + * @inheritdoc */ public function execute(string $path): AssetInterface { @@ -90,14 +77,11 @@ public function execute(string $path): AssetInterface $file = $this->getFileInfo->execute($absolutePath); [$width, $height] = getimagesize($absolutePath); - $metadata = $this->extractMetadata->execute($absolutePath); - return $this->assetFactory->create( [ 'id' => null, 'path' => $path, - 'title' => $metadata->getTitle() ?: $file->getBasename(), - 'description' => $metadata->getDescription(), + 'title' => $file->getBasename(), 'width' => $width, 'height' => $height, 'hash' => $this->getHash($path), diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php index 533d814c9f1d0..be672666786dd 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php @@ -12,6 +12,7 @@ use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface; /** * Create media asset object based on the file information @@ -29,19 +30,19 @@ class GetAssetFromPath private $assetFactory; /** - * @var CreateAssetFromFile + * @var CreateAssetFromFileInterface */ private $createAssetFromFile; /** * @param AssetInterfaceFactory $assetFactory * @param GetAssetsByPathsInterface $getMediaGalleryAssetByPath - * @param CreateAssetFromFile $createAssetFromFile + * @param CreateAssetFromFileInterface $createAssetFromFile */ public function __construct( AssetInterfaceFactory $assetFactory, GetAssetsByPathsInterface $getMediaGalleryAssetByPath, - CreateAssetFromFile $createAssetFromFile + CreateAssetFromFileInterface $createAssetFromFile ) { $this->assetFactory = $assetFactory; $this->getAssetsByPaths = $getMediaGalleryAssetByPath; diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php index 386798d68d9df..ec314416e36ee 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php @@ -33,13 +33,15 @@ public function __construct(PublisherInterface $publisher) } /** - * Publish media content synchronization message to the message queue. + * Publish media content synchronization message to the message queue + * + * @param array $paths */ - public function execute() : void + public function execute(array $paths = []) : void { $this->publisher->publish( self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION, - [self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION] + $paths ); } } diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php index 6c4338c0935dc..8a44307298065 100644 --- a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php @@ -9,14 +9,12 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\DriverInterface; -use Magento\MediaGalleryApi\Api\Data\AssetInterface; -use Magento\MediaGalleryApi\Api\Data\KeywordInterface; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; -use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -45,11 +43,6 @@ class SynchronizeFilesTest extends TestCase */ private $mediaDirectory; - /** - * @var GetAssetsKeywordsInterface - */ - private $getAssetKeywords; - /** * @inheritdoc */ @@ -58,7 +51,6 @@ protected function setUp(): void $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); $this->synchronizeFiles = Bootstrap::getObjectManager()->get(SynchronizeFilesInterface::class); $this->getAssetsByPath = Bootstrap::getObjectManager()->get(GetAssetsByPathsInterface::class); - $this->getAssetKeywords = Bootstrap::getObjectManager()->get(GetAssetsKeywordsInterface::class); $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) ->getDirectoryWrite(DirectoryList::MEDIA); } @@ -67,18 +59,16 @@ protected function setUp(): void * Test for SynchronizeFiles::execute * * @dataProvider filesProvider - * @param null|string $file - * @param null|string $title - * @param null|string $description - * @param null|array $keywords + * @param string $file + * @param string $title + * @param string $source * @throws FileSystemException - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function testExecute( - ?string $file, - ?string $title, - ?string $description, - ?array $keywords + string $file, + string $title, + string $source ): void { $path = realpath(__DIR__ . '/../_files/' . $file); $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($file); @@ -89,12 +79,10 @@ public function testExecute( $this->synchronizeFiles->execute([$file]); - $loadedAssets = $this->getAssetsByPath->execute([$file])[0]; - $loadedKeywords = $this->getKeywords($loadedAssets) ?: null; + $loadedAsset = $this->getAssetsByPath->execute([$file])[0]; - $this->assertEquals($title, $loadedAssets->getTitle()); - $this->assertEquals($description, $loadedAssets->getDescription()); - $this->assertEquals($keywords, $loadedKeywords); + $this->assertEquals($title, $loadedAsset->getTitle()); + $this->assertEquals($source, $loadedAsset->getSource()); $this->driver->deleteFile($modifiableFilePath); } @@ -110,42 +98,8 @@ public function filesProvider(): array [ '/magento.jpg', 'magento', - null, - null - ], - [ - '/magento_metadata.jpg', - 'Title of the magento image', - 'Description of the magento image', - [ - 'magento', - 'mediagallerymetadata' - ] + 'Local' ] ]; } - - /** - * Key asset keywords - * - * @param AssetInterface $asset - * @return string[] - */ - private function getKeywords(AssetInterface $asset): array - { - $assetKeywords = $this->getAssetKeywords->execute([$asset->getId()]); - - if (empty($assetKeywords)) { - return []; - } - - $keywords = current($assetKeywords)->getKeywords(); - - return array_map( - function (KeywordInterface $keyword) { - return $keyword->getKeyword(); - }, - $keywords - ); - } } diff --git a/app/code/Magento/MediaGallerySynchronization/composer.json b/app/code/Magento/MediaGallerySynchronization/composer.json index e1d4962366978..f9d642dd02568 100644 --- a/app/code/Magento/MediaGallerySynchronization/composer.json +++ b/app/code/Magento/MediaGallerySynchronization/composer.json @@ -6,8 +6,7 @@ "magento/framework": "*", "magento/module-media-gallery-api": "*", "magento/module-media-gallery-synchronization-api": "*", - "magento/framework-message-queue": "*", - "magento/module-media-gallery-metadata-api": "*" + "magento/framework-message-queue": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml index 47a4360575b2e..82bd1303eda74 100644 --- a/app/code/Magento/MediaGallerySynchronization/etc/di.xml +++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml @@ -9,11 +9,11 @@ <preference for="Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface" type="Magento\MediaGallerySynchronization\Model\Synchronize"/> <preference for="Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface" type="Magento\MediaGallerySynchronization\Model\FetchBatches"/> <preference for="Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface" type="Magento\MediaGallerySynchronization\Model\SynchronizeFiles"/> + <preference for="Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface" type="Magento\MediaGallerySynchronization\Model\CreateAssetFromFile"/> <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> <arguments> <argument name="importers" xsi:type="array"> - <item name="0" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportMediaAsset</item> - <item name="1" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportImageFileKeywords</item> + <item name="10" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportMediaAsset</item> </argument> </arguments> </type> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFileInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFileInterface.php new file mode 100644 index 0000000000000..667c2e68a27d8 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFileInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Create media asset object from the media file + */ +interface CreateAssetFromFileInterface +{ + /** + * Create media asset object from the media file + * + * @param string $path + * @return AssetInterface + * @throws FileSystemException + */ + public function execute(string $path): AssetInterface; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/composer.json b/app/code/Magento/MediaGallerySynchronizationApi/composer.json index 427bd2bd4aca7..19bab75dd5f42 100644 --- a/app/code/Magento/MediaGallerySynchronizationApi/composer.json +++ b/app/code/Magento/MediaGallerySynchronizationApi/composer.json @@ -3,7 +3,8 @@ "description": "Magento module responsible for the media gallery synchronization implementation API", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-media-gallery-api": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/LICENSE.txt b/app/code/Magento/MediaGallerySynchronizationMetadata/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/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/MediaGallerySynchronizationMetadata/LICENSE_AFL.txt b/app/code/Magento/MediaGallerySynchronizationMetadata/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/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/MediaGallerySynchronization/Model/ImportImageFileKeywords.php b/app/code/Magento/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php similarity index 93% rename from app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php rename to app/code/Magento/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php index 361137ad27686..a9910157f27c7 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php @@ -5,12 +5,11 @@ */ declare(strict_types=1); -namespace Magento\MediaGallerySynchronization\Model; +namespace Magento\MediaGallerySynchronizationMetadata\Model; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Filesystem\Driver\File; use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; use Magento\MediaGalleryApi\Api\Data\KeywordInterface; use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; @@ -22,18 +21,13 @@ /** * import image keywords from file metadata */ -class ImportImageFileKeywords implements ImportFilesInterface +class ImportKeywords implements ImportFilesInterface { /** * @var Filesystem */ private $filesystem; - /** - * @var File - */ - private $driver; - /** * @var KeywordInterfaceFactory */ @@ -60,7 +54,6 @@ class ImportImageFileKeywords implements ImportFilesInterface private $getAssetsByPaths; /** - * @param File $driver * @param Filesystem $filesystem * @param KeywordInterfaceFactory $keywordFactory * @param ExtractMetadataInterface $extractMetadata @@ -69,7 +62,6 @@ class ImportImageFileKeywords implements ImportFilesInterface * @param GetAssetsByPathsInterface $getAssetsByPaths */ public function __construct( - File $driver, Filesystem $filesystem, KeywordInterfaceFactory $keywordFactory, ExtractMetadataInterface $extractMetadata, @@ -77,7 +69,6 @@ public function __construct( AssetKeywordsInterfaceFactory $assetKeywordsFactory, GetAssetsByPathsInterface $getAssetsByPaths ) { - $this->driver = $driver; $this->filesystem = $filesystem; $this->keywordFactory = $keywordFactory; $this->extractMetadata = $extractMetadata; @@ -123,6 +114,7 @@ private function getMetadataKeywords(string $path): ?array { $metadataKeywords = $this->extractMetadata->execute($this->getMediaDirectory()->getAbsolutePath($path)) ->getKeywords(); + if ($metadataKeywords === null) { return null; } diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php b/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php new file mode 100644 index 0000000000000..59604c0b3e501 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationMetadata\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface; + +/** + * Add metadata to the asset created from file + */ +class CreateAssetFromFileMetadata +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @param Filesystem $filesystem + * @param AssetInterfaceFactory $assetFactory + * @param ExtractMetadataInterface $extractMetadata + */ + public function __construct( + Filesystem $filesystem, + AssetInterfaceFactory $assetFactory, + ExtractMetadataInterface $extractMetadata + ) { + $this->filesystem = $filesystem; + $this->assetFactory = $assetFactory; + $this->extractMetadata = $extractMetadata; + } + + /** + * Add metadata to the asset + * + * @param CreateAssetFromFileInterface $subject + * @param AssetInterface $asset + * @return AssetInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(CreateAssetFromFileInterface $subject, AssetInterface $asset): AssetInterface + { + $metadata = $this->extractMetadata->execute( + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($asset->getPath()) + ); + + return $this->assetFactory->create( + [ + 'id' => $asset->getId(), + 'path' => $asset->getPath(), + 'title' => $metadata->getTitle() ?: $asset->getTitle(), + 'description' => $metadata->getDescription(), + 'width' => $asset->getWidth(), + 'height' => $asset->getHeight(), + 'hash' => $asset->getHash(), + 'size' => $asset->getSize(), + 'contentType' => $asset->getContentType(), + 'source' => $asset->getSource() + ] + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/README.md b/app/code/Magento/MediaGallerySynchronizationMetadata/README.md new file mode 100644 index 0000000000000..64988dd543fe4 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGallerySynchronizationMetadata + +The purpose of this module is to include assets metadata to media gallery synchronization process diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json b/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json new file mode 100644 index 0000000000000..0674014026b24 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-gallery-synchronization-metadata", + "description": "Magento module responsible for images metadata synchronization", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-synchronization-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallerySynchronizationMetadata\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml b/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml new file mode 100644 index 0000000000000..ed66fd08cabfc --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/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\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> + <arguments> + <argument name="importers" xsi:type="array"> + <item name="20" xsi:type="object">Magento\MediaGallerySynchronizationMetadata\Model\ImportKeywords</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface"> + <plugin name="addMetadataToAssetCreatedFromFile" type="Magento\MediaGallerySynchronizationMetadata\Plugin\CreateAssetFromFileMetadata"/> + </type> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/etc/module.xml b/app/code/Magento/MediaGallerySynchronizationMetadata/etc/module.xml new file mode 100644 index 0000000000000..f92c370496d2d --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/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_MediaGallerySynchronizationMetadata"/> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/registration.php b/app/code/Magento/MediaGallerySynchronizationMetadata/registration.php new file mode 100644 index 0000000000000..82315db519f82 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGallerySynchronizationMetadata', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php new file mode 100644 index 0000000000000..d797acedda6ec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetails.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Block\Adminhtml; + +use Magento\Backend\Block\Template; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Image details block + * + * @api + */ +class ImageDetails extends Template +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var Json + */ + private $json; + + /** + * @param Template\Context $context + * @param AuthorizationInterface $authorization + * @param Json $json + * @param array $data + * @param JsonHelper|null $jsonHelper + * @param DirectoryHelper|null $directoryHelper + */ + public function __construct( + Template\Context $context, + AuthorizationInterface $authorization, + Json $json, + array $data = [], + ?JsonHelper $jsonHelper = null, + ?DirectoryHelper $directoryHelper = null + ) { + $this->authorization = $authorization; + $this->json = $json; + parent::__construct($context, $data, $jsonHelper, $directoryHelper); + } + + /** + * Retrieve actions json + * + * @return string + */ + public function getActionsJson(): string + { + $actions = [ + [ + 'title' => __('Cancel'), + 'handler' => 'closeModal', + 'name' => 'cancel', + 'classes' => 'action-default scalable cancel action-quaternary' + ] + ]; + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) { + $actions[] = [ + 'title' => __('Delete Image'), + 'handler' => 'deleteImageAction', + 'name' => 'delete', + 'classes' => 'action-default scalable delete action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { + $actions[] = [ + 'title' => __('Edit Details'), + 'handler' => 'editImageAction', + 'name' => 'edit', + 'classes' => 'action-default scalable edit action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::insert_assets')) { + $actions[] = [ + 'title' => __('Add Image'), + 'handler' => 'addImage', + 'name' => 'add-image', + 'classes' => 'scalable action-primary add-image-action' + ]; + } + + return $this->json->serialize($actions); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php new file mode 100644 index 0000000000000..7e73b1682f79a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Block/Adminhtml/ImageDetailsStandalone.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Block\Adminhtml; + +use Magento\Backend\Block\Template; +use Magento\Directory\Helper\Data as DirectoryHelperData; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Json\Helper\Data as JsonHelperData; +use Magento\Framework\Serialize\Serializer\Json; + +/** + * Image details block + * + * @api + */ +class ImageDetailsStandalone extends Template +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var Json + */ + private $json; + + /** + * @param Template\Context $context + * @param AuthorizationInterface $authorization + * @param Json $json + * @param array $data + * @param JsonHelperData|null $jsonHelper + * @param DirectoryHelperData|null $directoryHelper + */ + public function __construct( + Template\Context $context, + AuthorizationInterface $authorization, + Json $json, + array $data = [], + ?JsonHelperData $jsonHelper = null, + ?DirectoryHelperData $directoryHelper = null + ) { + $this->authorization = $authorization; + $this->json = $json; + parent::__construct($context, $data, $jsonHelper, $directoryHelper); + } + + /** + * Retrieve actions json + * + * @return string + */ + public function getActionsJson(): string + { + $standaloneActions = [ + [ + 'title' => __('Cancel'), + 'handler' => 'closeModal', + 'name' => 'cancel', + 'classes' => 'action-default scalable cancel action-quaternary' + ] + ]; + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::delete_assets')) { + $standaloneActions[] = [ + 'title' => __('Delete Image'), + 'handler' => 'deleteImageAction', + 'name' => 'delete', + 'classes' => 'action-default scalable delete action-quaternary' + ]; + } + + if ($this->authorization->isAllowed('Magento_MediaGalleryUiApi::edit_assets')) { + $standaloneActions[] = [ + 'title' => __('Edit Details'), + 'handler' => 'editImageAction', + 'name' => 'edit', + 'classes' => 'action-default scalable edit action-quaternary' + ]; + } + + return $this->json->serialize($standaloneActions); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php new file mode 100644 index 0000000000000..09837c301c367 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Asset; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; + +/** + * Controller to get selected asset for ui-select component + */ +class GetSelected extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetById; + + /** + * @var Images + */ + private $images; + + /** + * @var Storage + */ + private $storage; + + /** + * @param JsonFactory $resultFactory + * @param GetAssetsByIdsInterface $getAssetById + * @param Context $context + * @param Images $images + * @param Storage $storage + * + */ + public function __construct( + JsonFactory $resultFactory, + GetAssetsByIdsInterface $getAssetById, + Context $context, + Images $images, + Storage $storage + ) { + $this->resultJsonFactory = $resultFactory; + $this->getAssetById = $getAssetById; + $this->images = $images; + $this->storage = $storage; + parent::__construct($context); + } + + /** + * Return selected asset options. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $options = []; + $assetIds = $this->getRequest()->getParam('ids'); + + if (!is_array($assetIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + } + $assets = $this->getAssetById->execute($assetIds); + + foreach ($assets as $asset) { + $assetPath = $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()); + $options[] = [ + 'value' => (string) $asset->getId(), + 'label' => $asset->getTitle(), + 'src' => $assetPath + ]; + } + + return $this->resultJsonFactory->create()->setData($options); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php index 3d4af88e4ad67..76c00927b33e0 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -29,7 +29,7 @@ class Create extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::create_folder'; /** * @var CreateDirectoriesByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php index 56f12c5139d65..3dc43e5276860 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -30,7 +30,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_folder'; /** * @var DeleteAssetsByPathsInterface diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php index a5d1cee7abf41..2f7766c590033 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -31,7 +31,7 @@ class Delete extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::delete_assets'; /** * @var DeleteImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php index f41c489607b15..87a2e7345c407 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php @@ -32,7 +32,7 @@ class SaveDetails extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::edit_assets'; /** * @var UpdateAsset diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php index e965d94b33f0c..4492595bbe6ee 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -28,7 +28,7 @@ class Upload extends Action implements HttpPostActionInterface /** * @see _isAllowed() */ - public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + public const ADMIN_RESOURCE = 'Magento_MediaGalleryUiApi::upload_assets'; /** * @var UploadImage diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php index 3660374243d16..8c5b3d4d3a9ac 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php @@ -12,6 +12,9 @@ use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Controller\ResultInterface; +use Magento\MediaContentApi\Model\Config; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Forward; /** * Controller serving the media gallery content @@ -20,6 +23,24 @@ class Index extends Action implements HttpGetActionInterface { public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + /** + * @var Config + */ + private $config; + + /** + * Index constructor. + * @param Context $context + * @param Config $config + */ + public function __construct( + Context $context, + Config $config + ) { + parent::__construct($context); + $this->config = $config; + } + /** * Get the media gallery layout * @@ -27,6 +48,14 @@ class Index extends Action implements HttpGetActionInterface */ public function execute(): ResultInterface { + if (!$this->config->isEnabled()) { + /** @var Forward $resultForward */ + $resultForward = $this->resultFactory->create(ResultFactory::TYPE_FORWARD); + $resultForward->forward('noroute'); + + return $resultForward; + } + /** @var Page $resultPage */ $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); $resultPage->setActiveMenu('Magento_MediaGalleryUi::media_gallery') diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php index f0998a3e120f2..c22165ba4e51f 100644 --- a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php @@ -7,13 +7,14 @@ namespace Magento\MediaGalleryUi\Model\Directories; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\Read; use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; /** - * Build folder tree structure by path + * Build media gallery folder tree structure by path */ class GetFolderTree { @@ -22,43 +23,45 @@ class GetFolderTree */ private $filesystem; - /** - * @var string - */ - private $path; - /** * @var IsPathExcludedInterface */ private $isPathExcluded; /** - * Constructor - * * @param Filesystem $filesystem - * @param string $path * @param IsPathExcludedInterface $isPathExcluded */ public function __construct( Filesystem $filesystem, - string $path, IsPathExcludedInterface $isPathExcluded ) { $this->filesystem = $filesystem; - $this->path = $path; $this->isPathExcluded = $isPathExcluded; } /** * Return directory folder structure in array * - * @param bool $skipRoot * @return array * @throws ValidatorException */ - public function execute(bool $skipRoot = true): array + public function execute(): array { - return $this->buildFolderTree($this->getDirectories(), $skipRoot); + $tree = [ + 'name' => 'root', + 'path' => '/', + 'children' => [] + ]; + $directories = $this->getDirectories(); + foreach ($directories as $idx => &$node) { + $node['children'] = []; + $result = $this->findParent($node, $tree); + $parent = &$result['treeNode']; + + $parent['children'][] = &$directories[$idx]; + } + return $tree['children']; } /** @@ -72,7 +75,7 @@ private function getDirectories(): array $directories = []; /** @var Read $directory */ - $directory = $this->filesystem->getDirectoryRead($this->path); + $directory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); if (!$directory->isDirectory()) { return $directories; @@ -96,30 +99,6 @@ private function getDirectories(): array return $directories; } - /** - * Build folder tree structure by provided directories path - * - * @param array $directories - * @param bool $skipRoot - * @return array - */ - private function buildFolderTree(array $directories, bool $skipRoot): array - { - $tree = [ - 'name' => 'root', - 'path' => '/', - 'children' => [] - ]; - foreach ($directories as $idx => &$node) { - $node['children'] = []; - $result = $this->findParent($node, $tree); - $parent = & $result['treeNode']; - - $parent['children'][] =& $directories[$idx]; - } - return $skipRoot ? $tree['children'] : $tree; - } - /** * Find parent directory * diff --git a/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php new file mode 100644 index 0000000000000..e72017e20a7f6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Setup/Patch/Data/AddMediaGalleryPermissions.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; + +/** + * Patch is mechanism, that allows to do atomic upgrade data changes + */ +class AddMediaGalleryPermissions implements + DataPatchInterface, + PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface $moduleDataSetup + */ + private $moduleDataSetup; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup) + { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * Add child resources permissions for user roles with Magento_Cms::media_gallery permission + */ + public function apply(): void + { + $tableName = $this->moduleDataSetup->getTable('authorization_rule'); + $connection = $this->moduleDataSetup->getConnection(); + + if (!$tableName) { + return; + } + + $select = $connection->select() + ->from($tableName, ['role_id']) + ->where('resource_id = "Magento_Cms::media_gallery"'); + + $insertData = $this->getInsertData($connection->fetchCol($select)); + + if (!empty($insertData)) { + $connection->insertMultiple($tableName, $insertData); + } + } + + /** + * Retrieve data to insert to authorization_rule table based on role ids + * + * @param array $roleIds + * @return array + */ + private function getInsertData(array $roleIds): array + { + $newResources = [ + 'Magento_MediaGalleryUiApi::insert_assets', + 'Magento_MediaGalleryUiApi::upload_assets', + 'Magento_MediaGalleryUiApi::edit_assets', + 'Magento_MediaGalleryUiApi::delete_assets', + 'Magento_MediaGalleryUiApi::create_folder', + 'Magento_MediaGalleryUiApi::delete_folder' + ]; + + $data = []; + + foreach ($roleIds as $roleId) { + foreach ($newResources as $resourceId) { + $data[] = [ + 'role_id' => $roleId, + 'resource_id' => $resourceId, + 'permission' => 'allow' + ]; + } + } + + return $data; + } + + /** + * @inheritdoc + */ + public function getAliases(): array + { + return []; + } + + /** + * @inheritdoc + */ + public static function getDependencies(): array + { + return []; + } + + /** + * @inheritdoc + */ + public static function getVersion(): string + { + return '2.4.2'; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.xml new file mode 100644 index 0000000000000..af2b383143f62 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup.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="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup"> + <annotations> + <description>Validates that the provided elemen present on page but have attribute disabled.</description> + </annotations> + <arguments> + <argument name="buttonName" type="string"/> + </arguments> + + <grabMultiple selector="{{AdminEnhancedMediaGalleryActionsSection.notDisabledButtons}}" stepKey="verifyDisabledAttribute"/> + + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">verifyDisabledAttribute</actualResult> + <expectedResult type="array">[{{buttonName}}]</expectedResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.xml new file mode 100644 index 0000000000000..c212092b657fd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryEmptyActionGroup.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="AdminAssertMediaGalleryEmptyActionGroup"> + <annotations> + <description>Requires select folder in directory tree. Assert that selected folder is empty.</description> + </annotations> + + <seeElement selector="{{AdminMediaGalleryGridSection.noDataMessage}}" stepKey="assertNoDataMessageDisplayed" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml index 7f4db971702ca..ca503b7357300 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml @@ -12,7 +12,9 @@ <annotations> <description>Asserts images has been deleted in mass action.</description> </annotations> - - <see userInput='Assets have been successfully deleted' stepKey="verifyDeleteImages"/> + <arguments> + <argument name="numberOfAssetsDeleted" type="string"/> + </arguments> + <see userInput='{{numberOfAssetsDeleted}} assets have been successfully deleted.' stepKey="verifyDeleteImages"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml index a691f65387e8e..1ec2004b22f24 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml @@ -12,8 +12,8 @@ <annotations> <description>Asserts that massaction mode is terminated</description> </annotations> - + <dontSeeElement selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="verifyAddSelectedButtonNotVisible"/> <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMassAction"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickSortActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickSortActionGroup.xml new file mode 100644 index 0000000000000..7679da6585d5f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickSortActionGroup.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="AdminEnhancedMediaGalleryClickSortActionGroup"> + <arguments> + <argument name="sortName" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGallerySortBySection.sortDropdown}}" stepKey="clickOnSortDropdown"/> + <click selector="{{AdminEnhancedMediaGallerySortBySection.sortOption(sortName)}}" stepKey="clickOnSortOption"/> + <waitForPageLoad stepKey="waitForLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.xml new file mode 100644 index 0000000000000..db9d1853df583 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup.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="AdminEnhancedMediaGalleryExpandCatalogTmpFolderActionGroup"> + <annotations> + <description>Expand media gallery tmp folder tree</description> + </annotations> + <waitForLoadingMaskToDisappear stepKey="waitLoadingMask"/> + <conditionalClick selector="//li[@id='catalog']/ins" dependentSelector="//li[@id='catalog']/ul" visible="false" stepKey="expandCatalog"/> + <wait time="2" stepKey="waitCatalogExpanded"/> + <conditionalClick selector="//li[@id='catalog/tmp']/ins" dependentSelector="//li[@id='catalog/tmp']/ul" visible="false" stepKey="expandTmp"/> + <wait time="2" stepKey="waitTmpExpanded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml index 6785558c8ef54..08c93a805dc70 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml @@ -16,6 +16,5 @@ <argument name="title"/> </arguments> <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> - </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.xml new file mode 100644 index 0000000000000..49aa45426152c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectByFullPathActionGroup.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="AdminMediaGalleryFolderSelectByFullPathActionGroup"> + <arguments> + <argument name="path" type="string"/> + </arguments> + <wait time="2" stepKey="waitBeforeClickOnFolder"/> + <click selector="//li[@id='{{path}}']" stepKey="selectSubFolder" after="waitBeforeClickOnFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml index 6f38bd7c7d738..89664ef152dba 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml @@ -15,6 +15,6 @@ <conditionalClick stepKey="clickExpandContent" selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.selectFromGalleryButton}}" visible="false" /> <waitForElementVisible selector="{{AdminCategoryContentSection.selectFromGalleryButton}}" stepKey="waitForSelectFromGallery" /> <click selector="{{AdminCategoryContentSection.selectFromGalleryButton}}" stepKey="clickSelectFromGallery" /> - <waitForPageLoad stepKey="waitForPageLoad" /> + <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml new file mode 100644 index 0000000000000..53781a65e4898 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGallerySortByActionGroup.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminEnhancedMediaGallerySortByActionGroup"> + <annotations> + <description>Assert the images position in the grid after sorting has been applied.</description> + </annotations> + <arguments> + <argument name="firstImageFile" type="string"/> + <argument name="secondImageFile" type="string"/> + <argument name="thirdImageFile" type="string"/> + </arguments> + + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('0')}}" userInput="src" + stepKey="getFirstImageSrcAfterSort"/> + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('1')}}" userInput="src" + stepKey="getSecondImageSrcAfterSort"/> + <grabAttributeFrom selector="{{AdminMediaGalleryGridSection.nthImageInGrid('2')}}" userInput="src" + stepKey="getThirdImageSrcAfterSort"/> + + <assertStringContainsString stepKey="assertFirstImagePositionAfterSort"> + <actualResult type="string">{$getFirstImageSrcAfterSort}</actualResult> + <expectedResult type="string">{{firstImageFile}}</expectedResult> + </assertStringContainsString> + <assertStringContainsString stepKey="assertSecondImagePositionAfterSort"> + <actualResult type="string">{$getSecondImageSrcAfterSort}</actualResult> + <expectedResult type="string">{{secondImageFile}}</expectedResult> + </assertStringContainsString> + <assertStringContainsString stepKey="assertThirdImagePositionAfterSort"> + <actualResult type="string">{$getThirdImageSrcAfterSort}</actualResult> + <expectedResult type="string">{{thirdImageFile}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup.xml new file mode 100644 index 0000000000000..62adffc931c16 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup.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="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup"> + <annotations> + <description>Assert that's used in section not displayed in view details.</description> + </annotations> + + <dontSeeElement selector="{{AdminEnhancedMediaGalleryViewDetailsSection.usedIn}}" stepKey="assertImageIsDeleted"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertFolderIsChangedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertFolderIsChangedActionGroup.xml new file mode 100644 index 0000000000000..090dbed8b4f78 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertFolderIsChangedActionGroup.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="AssertFolderIsChangedActionGroup"> + <annotations> + <description>Assert that folder is changed</description> + </annotations> + <arguments> + <argument name="newSelectedFolder" type="string"/> + <argument name="oldSelectedFolder" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + + <assertNotEquals stepKey="assertNotEqual"> + <actualResult type="string">{{newSelectedFolder}}</actualResult> + <expectedResult type="string">{{oldSelectedFolder}}</expectedResult> + </assertNotEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml index 7f9a5aefdf69c..907f2c3116800 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml @@ -12,6 +12,7 @@ <element name="deleteViewButton" type="button" selector="//div[@data-bind='afterRender: \$data.setToolbarNode']//input/following-sibling::div/button[@class='action-delete']"/> <element name="upload" type="input" selector="#image-uploader-input"/> <element name="cancel" type="button" selector="[data-ui-id='cancel-button']"/> + <element name="notDisabledButtons" type="button" selector="//div[@class='page-actions floating-header']/button[not(@disabled='disabled') and not(@id='cancel')]"/> <element name="createFolder" type="button" selector="[data-ui-id='create-folder-button']"/> <element name="deleteFolder" type="button" selector="[data-ui-id='delete-folder-button']"/> <element name="imageSrc" type="text" selector="//div[@class='masonry-image-column' and contains(@data-repeat-index, '0')]//img[contains(@src,'{{src}}')]" parameterized="true"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGallerySortBySection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGallerySortBySection.xml new file mode 100644 index 0000000000000..5ffcec00fcf4b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGallerySortBySection.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="AdminEnhancedMediaGallerySortBySection"> + <element name="sortDropdown" type="button" selector="div[class='masonry-image-sortby'] select"/> + <element name="sortOption" type="button" selector="//div[@class='masonry-image-sortby'] //option[@value='{{sortOption}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml index e63429677fbae..d6abe464048c7 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml @@ -19,6 +19,7 @@ <element name="delete" type="button" selector="//div[@class='media-gallery-image-details-modal']//button[contains(@class, 'delete')]"/> <element name="confirmDelete" type="button" selector=".action-accept"/> <element name="createdAtDate" type="button" selector="//div[@class='attribute']/span[contains(text(), 'Created')]/following-sibling::div"/> + <element name="usedIn" type="button" selector="//div[@class='attribute']/span[contains(text(), 'Used In')]"/> <element name="updatedAtDate" type="button" selector="//div[@class='attribute']/span[contains(text(), 'Modified')]/following-sibling::div"/> <element name="addImage" type="button" selector=".add-image-action"/> <element name="cancel" type="button" selector="#image-details-action-cancel"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml new file mode 100644 index 0000000000000..f35a32b6d3a37 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.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="AdminMediaGalleryGridSection"> + <element name="noDataMessage" type="text" selector="div.no-data-message-container"/> + <element name="nthImageInGrid" type="text" selector="div[class='masonry-image-column'][data-repeat-index='{{row}}'] img" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiDisabledSuite.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiDisabledSuite.xml new file mode 100644 index 0000000000000..727fbde3f17b6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiDisabledSuite.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> + <suite name="MediaGalleryUiDisabledSuite"> + <include> + <group name="media_gallery_ui_disabled"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml index 94831b039b53a..fe2b5b1639fbe 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml @@ -19,9 +19,17 @@ <group value="media_gallery_ui"/> </annotations> <before> + <createData entity="SimpleSubCategory" stepKey="category"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> <argument name="image" value="ImageUpload"/> </actionGroup> @@ -34,7 +42,7 @@ <argument name="imageName" value="{{ImageUpload.fileName}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryDisableMassactionModeActionGroup" stepKey="disableMassActionMode"/> - + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> <argument name="imageName" value="{{ImageUpload.fileName}}"/> @@ -44,7 +52,9 @@ </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> - <actionGroup ref="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup" stepKey="assertImagesDeleted"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup" stepKey="assertImagesDeleted"> + <argument name="numberOfAssetsDeleted" value="2"/> + </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup" stepKey="assertMassectionModeDisabled"/> </test> </tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml index bd7e4fcf7a9a2..9a08f7cd0bb9c 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml @@ -65,16 +65,21 @@ <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryGridPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="firstResetAdminDataGridToDefaultView"/> <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> <argument name="filterName" value="Asset"/> <argument name="optionName" value="{{ImageMetadata.title}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> - - <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> - <argument name="categoryName" value="$$category.name$$"/> + <actionGroup ref="AssertAdminCategoryGridPageNumberOfRecordsActionGroup" stepKey="assertOneRecordInGrid"> + <argument name="numberOfRecords" value="1 records found"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageImageColumnActionGroup" stepKey="assertCategoryGridPageImageColumn"/> + <actionGroup ref="AssertAdminCategoryGridPageDetailsActionGroup" stepKey="assertCategoryInGrid"> + <argument name="category" value="$$category$$"/> </actionGroup> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup" stepKey="assertCategoryGridPageProductsInMenuEnabledColumns"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="secondResetAdminDataGridToDefaultView"/> </test> </tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.xml new file mode 100644 index 0000000000000..9738ddedc3cc3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderAclTest.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="AdminMediaGalleryCreateFolderAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery cretae folder functionality"/> + <description value="User manages ACL rules for Media Gallery cretae folder functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Create folder"/> + </actionGroup> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Create Folder"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.xml new file mode 100644 index 0000000000000..1d51caf0fc400 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteAssetsAclTest.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="AdminMediaGalleryDeleteAssetsAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery delete assets functionality"/> + <description value="User manages ACL rules for Media Gallery delete assets functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="uncheckDeleteFolder"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete assets"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Delete Images..."/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.xml new file mode 100644 index 0000000000000..121ad25c93f0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteFolderAclTest.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="AdminMediaGalleryDeleteFolderAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery delete folder functionality"/> + <description value="User manages ACL rules for Media Gallery delete folder functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Delete folder"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Delete Folder"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml index 963a0b954e45b..5926b115afccf 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml @@ -9,6 +9,9 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryDisabledContentFilterTest"> <annotations> + <skip> + <issueId value="https://github.com/magento/adobe-stock-integration/issues/1825"/> + </skip> <features value="MediaGallery"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> <title value="User filter asset by disabled content"/> @@ -58,8 +61,8 @@ <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> <argument name="imageName" value="{{ImageMetadata.title}}"/> </actionGroup> - <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clickDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> - + </test> </tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByDirectoryAscendingTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByDirectoryAscendingTest.xml new file mode 100644 index 0000000000000..4dbf3da0752b2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByDirectoryAscendingTest.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGallerySortByDirectoryAscendingTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <title value="User uses Sort by Directory Ascending in Standalone Media Gallery"/> + <stories value="User uses Sort by Directory Ascending in Standalone Media Gallery"/> + <testCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <description value="User uses Sort by Directory Ascending in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFirstFolderForDelete"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFirstFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFirstFolderWasDeleted"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectSecondFolderForDelete"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteSecondFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertSecondFolderWasDeleted"> + <argument name="name" value="secondFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openFirstNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createFirstNewFolder"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFirstNewFolderCreated"> + <argument name="name" value="firstFolder"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openSecondNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createSecondNewFolder"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertSecondNewFolderCreated"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="secondResetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="secondWaitForGridToLoad"/> + + <actionGroup ref="AdminEnhancedMediaGalleryClickSortActionGroup" stepKey="sortByDirectoryAscending"> + <argument name="sortName" value="directory_asc"/> + </actionGroup> + + <actionGroup ref="AssertAdminEnhancedMediaGallerySortByActionGroup" stepKey="assertImagePositionAfterSortByDirectoryAscending"> + <argument name="firstImageFile" value="{{ImageUpload_1.file}}"/> + <argument name="secondImageFile" value="{{ImageUpload.file}}"/> + <argument name="thirdImageFile" value="{{ImageUpload1.value}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByDirectoryDescendingTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByDirectoryDescendingTest.xml new file mode 100644 index 0000000000000..025da24511b73 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByDirectoryDescendingTest.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="AdminMediaGallerySortByDirectoryDescendingTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <title value="User uses Sort by Directory Descending in Standalone Media Gallery"/> + <stories value="User uses Sort by Directory Descending in Standalone Media Gallery"/> + <testCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <description value="User uses Sort by Directory Descending in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFirstFolderForDelete"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFirstFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFirstFolderWasDeleted"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectSecondFolderForDelete"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteSecondFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertSecondFolderWasDeleted"> + <argument name="name" value="secondFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openFirstNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createFirstNewFolder"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFirstNewFolderCreated"> + <argument name="name" value="firstFolder"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openSecondNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createSecondNewFolder"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertSecondNewFolderCreated"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="secondResetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="secondWaitForGridToLoad"/> + + <actionGroup ref="AdminEnhancedMediaGalleryClickSortActionGroup" stepKey="sortByDirectoryDescending"> + <argument name="sortName" value="directory_desc"/> + </actionGroup> + + <actionGroup ref="AssertAdminEnhancedMediaGallerySortByActionGroup" stepKey="assertImagePositionAfterSortByDirectoryDescending"> + <argument name="firstImageFile" value="{{ImageUpload1.value}}"/> + <argument name="secondImageFile" value="{{ImageUpload.file}}"/> + <argument name="thirdImageFile" value="{{ImageUpload_1.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNameAToZTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNameAToZTest.xml new file mode 100644 index 0000000000000..da0d8a18b75e4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNameAToZTest.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGallerySortByNameAToZTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <title value="User uses Sort by Name A to Z in Standalone Media Gallery"/> + <stories value="User uses Sort by Name A to Z in Standalone Media Gallery"/> + <testCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <description value="User uses Sort by Name A to Z in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFirstFolderForDelete"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFirstFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFirstFolderWasDeleted"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectSecondFolderForDelete"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteSecondFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertSecondFolderWasDeleted"> + <argument name="name" value="secondFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openFirstNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createFirstNewFolder"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFirstNewFolderCreated"> + <argument name="name" value="firstFolder"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openSecondNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createSecondNewFolder"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertSecondNewFolderCreated"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="secondResetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="secondWaitForGridToLoad"/> + + <actionGroup ref="AdminEnhancedMediaGalleryClickSortActionGroup" stepKey="sortByNameAToZ"> + <argument name="sortName" value="name_az"/> + </actionGroup> + + <actionGroup ref="AssertAdminEnhancedMediaGallerySortByActionGroup" stepKey="assertImagePositionAfterSortByNameAToZ"> + <argument name="firstImageFile" value="{{ImageUpload.file}}"/> + <argument name="secondImageFile" value="{{ImageUpload_1.file}}"/> + <argument name="thirdImageFile" value="{{ImageUpload1.value}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNameZToATest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNameZToATest.xml new file mode 100644 index 0000000000000..4b5086e5d63ff --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNameZToATest.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGallerySortByNameZToATest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <title value="User uses Sort by Name Z to A in Standalone Media Gallery"/> + <stories value="User uses Sort by Name Z to A in Standalone Media Gallery"/> + <testCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <description value="User uses Sort by Name Z to A in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFirstFolderForDelete"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFirstFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFirstFolderWasDeleted"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectSecondFolderForDelete"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteSecondFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertSecondFolderWasDeleted"> + <argument name="name" value="secondFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openFirstNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createFirstNewFolder"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFirstNewFolderCreated"> + <argument name="name" value="firstFolder"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openSecondNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createSecondNewFolder"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertSecondNewFolderCreated"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="secondResetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="secondWaitForGridToLoad"/> + + <actionGroup ref="AdminEnhancedMediaGalleryClickSortActionGroup" stepKey="sortByNameZToA"> + <argument name="sortName" value="name_za"/> + </actionGroup> + + <actionGroup ref="AssertAdminEnhancedMediaGallerySortByActionGroup" stepKey="assertImagePositionAfterSortByNameZToA"> + <argument name="firstImageFile" value="{{ImageUpload1.value}}"/> + <argument name="secondImageFile" value="{{ImageUpload_1.file}}"/> + <argument name="thirdImageFile" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNewestFirstTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNewestFirstTest.xml new file mode 100644 index 0000000000000..4274b26d5770f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByNewestFirstTest.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="AdminMediaGallerySortByNewestFirstTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <title value="User uses Sort by Newest First in Standalone Media Gallery"/> + <stories value="User uses Sort by Newest First in Standalone Media Gallery"/> + <testCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <description value="User uses Sort by Newest First in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFirstFolderForDelete"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFirstFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFirstFolderWasDeleted"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectSecondFolderForDelete"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteSecondFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertSecondFolderWasDeleted"> + <argument name="name" value="secondFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openFirstNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createFirstNewFolder"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFirstNewFolderCreated"> + <argument name="name" value="firstFolder"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openSecondNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createSecondNewFolder"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertSecondNewFolderCreated"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="secondResetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="secondWaitForGridToLoad"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickSortActionGroup" stepKey="sortByNewestFirst"> + <argument name="sortName" value="newest_first"/> + </actionGroup> + <actionGroup ref="AssertAdminEnhancedMediaGallerySortByActionGroup" stepKey="assertImagePositionAfterSortByNewestFirst"> + <argument name="firstImageFile" value="{{ImageUpload1.value}}"/> + <argument name="secondImageFile" value="{{ImageUpload_1.file}}"/> + <argument name="thirdImageFile" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByOldestFirstTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByOldestFirstTest.xml new file mode 100644 index 0000000000000..e67fdcfcf40b3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySortByOldestFirstTest.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="AdminMediaGallerySortByOldestFirstTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <title value="User uses Sort by Oldest First in Standalone Media Gallery"/> + <stories value="User uses Sort by Oldest First in Standalone Media Gallery"/> + <testCaseId value="https://github.com/magento/adobe-stock-integration/issues/1776"/> + <description value="User uses Sort by Oldest First in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFirstFolderForDelete"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFirstFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFirstFolderWasDeleted"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectSecondFolderForDelete"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteSecondFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertSecondFolderWasDeleted"> + <argument name="name" value="secondFolder"/> + </actionGroup> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openFirstNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createFirstNewFolder"> + <argument name="name" value="firstFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFirstNewFolderCreated"> + <argument name="name" value="firstFolder"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="waitForGridToLoad"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openSecondNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createSecondNewFolder"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertSecondNewFolderCreated"> + <argument name="name" value="secondFolder"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadThirdImage"> + <argument name="image" value="ImageUpload1"/> + </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="secondResetAdminDataGridToDefaultView"/> + <waitForPageLoad stepKey="secondWaitForGridToLoad"/> + + <actionGroup ref="AdminEnhancedMediaGalleryClickSortActionGroup" stepKey="sortByOldestFirst"> + <argument name="sortName" value="oldest_first"/> + </actionGroup> + <actionGroup ref="AssertAdminEnhancedMediaGallerySortByActionGroup" stepKey="assertImagePositionAfterSortByOldestFirst"> + <argument name="firstImageFile" value="{{ImageUpload.file}}"/> + <argument name="secondImageFile" value="{{ImageUpload_1.file}}"/> + <argument name="thirdImageFile" value="{{ImageUpload1.value}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySwitchingBetweenViewsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySwitchingBetweenViewsTest.xml new file mode 100644 index 0000000000000..01b8c27b7371d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySwitchingBetweenViewsTest.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="AdminMediaGallerySwitchingBetweenViewsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1523"/> + <title value="User switches between Views and checks if the folder is changed"/> + <stories value="User switches between Views and checks if the folder is changed"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/5060037"/> + <description value="User switches between Views and checks if the folder is changed"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeleteGridViewActionGroup" stepKey="deleteView"> + <argument name="viewToDelete" value="New View"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> + <actionGroup ref="AdminEnhancedMediaGallerySaveCustomViewActionGroup" stepKey="saveCustomView"> + <argument name="viewName" value="New View"/> + </actionGroup> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="selectDefaultView"> + <argument name="selectView" value="Default View"/> + </actionGroup> + <actionGroup ref="AssertFolderIsChangedActionGroup" stepKey="assertFolderIsChanged"> + <argument name="newSelectedFolder" value="category" /> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="switchBackToNewView"> + <argument name="selectView" value="New View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup" stepKey="assertFilterApplied"> + <argument name="resultValue" value="{{AdminMediaGalleryFolderData.name}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.xml new file mode 100644 index 0000000000000..c8f8655d11edb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadAssetsAclTest.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="AdminMediaGalleryUploadAssetsAclTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="[Story 60] User manages ACL rules for Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1485"/> + <title value="User manages ACL rules for Media Gallery upload assets functionality"/> + <description value="User manages ACL rules for Media Gallery upload assets functionality"/> + <testCaseId value="https://app.hiptest.com/projects/131313/test-plan/folders/943908/scenarios/3218882"/> + <severity value="MAJOR"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminBefore"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminAfter"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> + <argument name="role" value="adminRole"/> + </actionGroup> + <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> + <waitForPageLoad stepKey="waitForUsersGridLoad" /> + <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> + <argument name="userName" value="{{admin2.username}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminFillUserRoleRequiredDataActionGroup" stepKey="fillUserRoleRequiredData"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Media Gallery"/> + </actionGroup> + <actionGroup ref="AdminUserClickRoleResourceTabActionGroup" stepKey="switchToRoleResourceTab"/> + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryUnchekDeleteAssets"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Upload assets"/> + </actionGroup> + + <actionGroup ref="AdminAddRestrictedRoleActionGroup" stepKey="AddMediaGalleryPagesResource"> + <argument name="User" value="adminRole"/> + <argument name="restrictedRole" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminUserSaveRoleActionGroup" stepKey="saveRole"/> + + <actionGroup ref="AdminCreateUserActionGroup" stepKey="createAdminUser"> + <argument name="role" value="adminRole"/> + <argument name="User" value="admin2"/> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{admin2.username}}"/> + <argument name="password" value="{{admin2.password}}"/> + </actionGroup> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminAssertMediaGalleryButtonNotDisabledOnPageActionGroup" stepKey="assertCreateButtonEnabledAllOthersDisabled"> + <argument name="buttonName" value="Upload Image"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml index ca7a71258fead..684db1d4a2627 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml @@ -36,8 +36,15 @@ <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCatalogFolder"> + <argument name="name" value="catalog"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCategoryFolder"> + <argument name="name" value="category"/> + </actionGroup> <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> - <argument name="title" value="ProductImage.filename"/> + <argument name="title" value="ProductImage.fileName"/> </actionGroup> </test> </tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml new file mode 100644 index 0000000000000..8b0c984c1df77 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryDisabledTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1760"/> + <title value="Standalone Media Gallery Page should return 404 if Media Gallery is disabled"/> + <stories value="#1760 Media Gallery Page opened successfully if Enhanced Media Gallery disabled"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/5106786"/> + <description value="Standalone Media Gallery Page should return 404 if Media Gallery is disabled"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui_disabled"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AssertAdminPageIs404ActionGroup" stepKey="see404Page"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php new file mode 100644 index 0000000000000..039a1006c79e5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/CreateFolder.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Create Folder button + */ +class CreateFolder implements ButtonProviderInterface +{ + private const ACL_CREATE_FOLDER = 'Magento_MediaGalleryUiApi::create_folder'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Create Folder'), + 'on_click' => 'jQuery("#create_folder").trigger("create_folder");', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 10, + ]; + + if (!$this->authorization->isAllowed(self::ACL_CREATE_FOLDER)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php new file mode 100644 index 0000000000000..10604d65f768f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteAssets.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Delete images button + */ +class DeleteAssets implements ButtonProviderInterface +{ + private const ACL_DELETE_ASSETS= 'Magento_MediaGalleryUiApi::delete_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Delete Images...'), + 'on_click' => 'jQuery(window).trigger("massAction.MediaGallery")', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 50, + ]; + + if (!$this->authorization->isAllowed(self::ACL_DELETE_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php new file mode 100644 index 0000000000000..cb803c1c663e0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/DeleteFolder.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Delete Folder button + */ +class DeleteFolder implements ButtonProviderInterface +{ + private const ACL_DELETE_FOLDER = 'Magento_MediaGalleryUiApi::delete_folder'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Delete Folder'), + 'disabled' => 'disabled', + 'on_click' => 'jQuery("#delete_folder").trigger("delete_folder");', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 30, + ]; + if (!$this->authorization->isAllowed(self::ACL_DELETE_FOLDER)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php new file mode 100644 index 0000000000000..6854b79ba2c36 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/InsertAsstes.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Add selected button + */ +class InsertAsstes implements ButtonProviderInterface +{ + private const ACL_INSERT_ASSETS = 'Magento_MediaGalleryUiApi::insert_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Add Selected'), + 'on_click' => 'return false;', + 'class' => 'action-primary no-display media-gallery-add-selected', + 'sort_order' => 110, + ]; + + if (!$this->authorization->isAllowed(self::ACL_INSERT_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php new file mode 100644 index 0000000000000..32bbdba88a599 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Control/UploadAssets.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryUi\Ui\Component\Control; + +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Upload Image button + */ +class UploadAssets implements ButtonProviderInterface +{ + private const ACL_UPLOAD_ASSETS= 'Magento_MediaGalleryUiApi::upload_assets'; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor. + * + * @param AuthorizationInterface $authorization + */ + public function __construct( + AuthorizationInterface $authorization + ) { + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function getButtonData(): array + { + $buttonData = [ + 'label' => __('Upload Image'), + 'on_click' => 'jQuery("#image-uploader-input").click();', + 'class' => 'action-default scalable add media-gallery-actions-buttons', + 'sort_order' => 20, + ]; + + if (!$this->authorization->isAllowed(self::ACL_UPLOAD_ASSETS)) { + $buttonData['disabled'] = 'disabled'; + } + + return $buttonData; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php similarity index 52% rename from app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php rename to app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php index 4047a4fcb98d8..0ad5ad43f6157 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -10,33 +10,46 @@ use Magento\Framework\UrlInterface; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Ui\Component\Container; +use Magento\Framework\AuthorizationInterface; /** * Directories tree component */ -class DirectoriesTree extends Container +class DirectoryTree extends Container { + private const ACL_IMAGE_ACTIONS = [ + 'delete_folder' => 'Magento_MediaGalleryUiApi::delete_folder' + ]; + /** * @var UrlInterface */ private $url; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * Constructor * * @param ContextInterface $context * @param UrlInterface $url + * @param AuthorizationInterface $authorization * @param array $components * @param array $data */ public function __construct( ContextInterface $context, UrlInterface $url, + AuthorizationInterface $authorization, array $components = [], array $data = [] ) { parent::__construct($context, $components, $data); $this->url = $url; + $this->authorization = $authorization; } /** @@ -50,11 +63,27 @@ public function prepare(): void array_replace_recursive( (array) $this->getData('config'), [ - 'getDirectoryTreeUrl' => $this->url->getUrl("media_gallery/directories/gettree"), - 'deleteDirectoryUrl' => $this->url->getUrl("media_gallery/directories/delete"), - 'createDirectoryUrl' => $this->url->getUrl("media_gallery/directories/create") + 'allowedActions' => $this->getAllowedActions(), + 'getDirectoryTreeUrl' => $this->url->getUrl('media_gallery/directories/gettree'), + 'deleteDirectoryUrl' => $this->url->getUrl('media_gallery/directories/delete'), + 'createDirectoryUrl' => $this->url->getUrl('media_gallery/directories/create') ] ) ); } + + /** + * Return allowed actions for media gallery + */ + private function getAllowedActions(): array + { + $allowedActions = []; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } + } + + return $allowedActions; + } } diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php index 481f8ab861f0f..0d48a0d0ff0e1 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -15,12 +15,20 @@ use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\StoreManagerInterface; use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\AuthorizationInterface; /** * Overlay column */ class Url extends Column { + private const ACL_IMAGE_ACTIONS = [ + 'image-details' => 'Magento_Cms::media_gallery', + 'insert' => 'Magento_MediaGalleryUiApi::insert_assets', + 'delete' => 'Magento_MediaGalleryUiApi::delete_assets', + 'edit' => 'Magento_MediaGalleryUiApi::edit_assets' + ]; + /** * @var StoreManagerInterface */ @@ -41,6 +49,11 @@ class Url extends Column */ private $storage; + /** + * @var AuthorizationInterface + */ + private $authorization; + /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory @@ -48,6 +61,7 @@ class Url extends Column * @param UrlInterface $urlInterface * @param Images $images * @param Storage $storage + * @param AuthorizationInterface $authorization * @param array $components * @param array $data */ @@ -58,6 +72,7 @@ public function __construct( UrlInterface $urlInterface, Images $images, Storage $storage, + AuthorizationInterface $authorization, array $components = [], array $data = [] ) { @@ -66,6 +81,7 @@ public function __construct( $this->urlInterface = $urlInterface; $this->images = $images; $this->storage = $storage; + $this->authorization = $authorization; } /** @@ -98,6 +114,7 @@ public function prepare(): void array_replace_recursive( (array)$this->getData('config'), [ + 'allowedActions' => $this->getAllowedActions(), 'onInsertUrl' => $this->urlInterface->getUrl('cms/wysiwyg_images/oninsert'), 'storeId' => $this->storeManager->getStore()->getId() ] @@ -105,6 +122,21 @@ public function prepare(): void ); } + /** + * Return allowed actions for media gallery image + */ + private function getAllowedActions(): array + { + $allowedActions = []; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } + } + + return $allowedActions; + } + /** * Get URL for the provided media asset path * diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php index e8dc232584adb..f61e34512bfe3 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -15,10 +15,6 @@ use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\Select; -use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; -use Magento\Cms\Helper\Wysiwyg\Images; -use Magento\Cms\Model\Wysiwyg\Images\Storage; -use Magento\Ui\Api\BookmarkManagementInterface; /** * Asset filter @@ -30,26 +26,6 @@ class Asset extends Select */ private $getContentIdentities; - /** - * @var GetAssetsByIdsInterface - */ - private $getAssetsByIds; - - /** - * @var Images - */ - private $images; - - /** - * @var Storage - */ - private $storage; - - /** - * @var BookmarkManagementInterface - */ - private $bookmarkManagement; - /** * Constructor * @@ -59,10 +35,6 @@ class Asset extends Select * @param FilterModifier $filterModifier * @param OptionSourceInterface $optionsProvider * @param GetContentByAssetIdsInterface $getContentIdentities - * @param GetAssetsByIdsInterface $getAssetsByIds - * @param BookmarkManagementInterface $bookmarkManagement - * @param Images $images - * @param Storage $storage * @param array $components * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -74,10 +46,6 @@ public function __construct( FilterModifier $filterModifier, OptionSourceInterface $optionsProvider = null, GetContentByAssetIdsInterface $getContentIdentities, - GetAssetsByIdsInterface $getAssetsByIds, - BookmarkManagementInterface $bookmarkManagement, - Images $images, - Storage $storage, array $components = [], array $data = [] ) { @@ -93,89 +61,6 @@ public function __construct( $data ); $this->getContentIdentities = $getContentIdentities; - $this->getAssetsByIds = $getAssetsByIds; - $this->bookmarkManagement = $bookmarkManagement; - $this->images = $images; - $this->storage = $storage; - } - - /** - * Prepare component configuration - * - * @return void - */ - public function prepare() - { - $options = []; - $assetIds = $this->getAssetIds(); - - if (empty($assetIds)) { - parent::prepare(); - return; - } - - $assets = $this->getAssetsByIds->execute($assetIds); - - foreach ($assets as $asset) { - $assetPath = $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()); - $options[] = [ - 'value' => (string) $asset->getId(), - 'label' => $asset->getTitle(), - 'src' => $assetPath - ]; - } - - $this->optionsProvider = $options; - parent::prepare(); - } - - /** - * Get asset ids from filterData or from bookmarks - */ - private function getAssetIds(): array - { - $assetIds = []; - - if (isset($this->filterData[$this->getName()])) { - $assetIds = $this->filterData[$this->getName()]; - - if (!is_array($assetIds)) { - $assetIds = $this->stringToArray($assetIds); - } - - return $assetIds; - } - - $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( - 'current', - $this->context->getNameSpace() - ); - - if ($bookmark === null) { - return $assetIds; - } - - $applied = $bookmark->getConfig()['current']['filters']['applied']; - - if (isset($applied[$this->getName()])) { - $assetIds = $applied[$this->getName()]; - } - - if (!is_array($assetIds)) { - $assetIds = $this->stringToArray($assetIds); - } - - return $assetIds; - } - - /** - * Converts string array from url-applier to array - * - * @param string $string - */ - private function stringToArray(string $string): array - { - return explode(',', str_replace(['[', ']'], '', $string)); } /** @@ -191,7 +76,7 @@ public function applyFilter() $assetIds = $this->filterData[$this->getName()]; if (!is_array($assetIds)) { - $assetIds = $this->stringToArray($assetIds); + $assetIds = explode(',', str_replace(['[', ']'], '', $assetIds)); } $filter = $this->filterBuilder->setConditionType('in') diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php new file mode 100644 index 0000000000000..7d7b67125df96 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Massactions/Massaction.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Massactions; + +use Magento\Ui\Component\Container; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\AuthorizationInterface; + +/** + * Massaction comntainer + */ +class Massaction extends Container +{ + private const ACL_IMAGE_ACTIONS = [ + 'delete_assets' => 'Magento_MediaGalleryUiApi::delete_assets' + ]; + + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor + * + * @param ContextInterface $context + * @param AuthorizationInterface $authorization + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + AuthorizationInterface $authorization, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->authorization = $authorization; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array)$this->getData('config'), + [ + 'allowedActions' => $this->getAllowedActions() + ] + ) + ); + } + + /** + * Return allowed actions for media gallery + */ + private function getAllowedActions(): array + { + $allowedActions = []; + foreach (self::ACL_IMAGE_ACTIONS as $key => $action) { + if ($this->authorization->isAllowed($action)) { + $allowedActions[] = $key; + } + } + + return $allowedActions; + } +} diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json index f4701306eb369..204e0b37c3bf8 100644 --- a/app/code/Magento/MediaGalleryUi/composer.json +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -12,7 +12,9 @@ "magento/module-media-gallery-metadata-api": "*", "magento/module-media-gallery-synchronization-api": "*", "magento/module-media-content-api": "*", - "magento/module-cms": "*" + "magento/module-cms": "*", + "magento/module-directory": "*", + "magento/module-authorization": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGalleryUi/etc/di.xml b/app/code/Magento/MediaGalleryUi/etc/di.xml index a8c4e2a8d8963..6ed3a98bbf03a 100644 --- a/app/code/Magento/MediaGalleryUi/etc/di.xml +++ b/app/code/Magento/MediaGalleryUi/etc/di.xml @@ -28,11 +28,6 @@ </argument> </arguments> </type> - <type name="Magento\MediaGalleryUi\Model\Directories\GetFolderTree"> - <arguments> - <argument name="path" xsi:type="string">media</argument> - </arguments> - </type> <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> <plugin name="createMediaGalleryThumbnails" type="Magento\MediaGalleryUi\Plugin\CreateThumbnails"/> </type> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml index f41c0f91b2249..a5eb247bd344f 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -16,7 +16,7 @@ <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> </container> <uiComponent name="media_gallery_listing"/> - <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details.phtml"> + <block name="image.details" class="Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails" template="Magento_MediaGalleryUi::image_details.phtml"> <arguments> <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> </arguments> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml index 7750f22b39ce7..b4f377627c850 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml @@ -10,7 +10,7 @@ <body> <referenceContainer htmlTag="div" htmlClass="media-gallery-container" name="content"> <uiComponent name="standalone_media_gallery_listing"/> - <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> + <block name="image.details" class="Magento\MediaGalleryUi\Block\Adminhtml\ImageDetailsStandalone" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> <arguments> <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> </arguments> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml index ba2033478afa1..5df5c1a6c4cbd 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -4,11 +4,11 @@ * See COPYING.txt for license details. */ -use Magento\Backend\Block\Template; +use Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails; use Magento\Framework\Escaper; // phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength -/** @var Template $block */ +/** @var ImageDetails $block */ /** @var Escaper $escaper */ ?> @@ -73,37 +73,10 @@ use Magento\Framework\Escaper; "modalWindowSelector": ".media-gallery-image-details", "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", - "actionsList": [ - { - "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", - "handler": "editImageAction", - "name": "edit", - "classes": "action-default scalable edit action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", - "handler": "closeModal", - "name": "cancel", - "classes": "action-default scalable cancel action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", - "handler": "deleteImageAction", - "name": "delete", - "classes": "action-default scalable delete action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Add Image')); ?>", - "handler": "addImage", - "name": "add-image", - "classes": "scalable action-primary add-image-action" - } - ] + "actionsList": <?= /* @noEscape */ $block->getActionsJson() ?> } } } } } </script> - - diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml index 9fc0e749ac888..fdae0a549606c 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -4,10 +4,8 @@ * See COPYING.txt for license details. */ -use Magento\Backend\Block\Template; - // phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength -/** @var Template $block */ +/** @var \Magento\MediaGalleryUi\Block\Adminhtml\ImageDetails $block */ /** @var \Magento\Framework\Escaper $escaper */ ?> @@ -71,31 +69,10 @@ use Magento\Backend\Block\Template; "modalWindowSelector": ".media-gallery-image-details", "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", - "actionsList": [ - { - "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", - "handler": "editImageAction", - "name": "edit", - "classes": "action-default scalable edit action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", - "handler": "closeModal", - "name": "cancel", - "classes": "action-default scalable cancel action-quaternary" - }, - { - "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", - "handler": "deleteImageAction", - "name": "delete", - "classes": "action-default scalable delete action-quaternary" - } - ] + "actionsList": <?= /* @noEscape */ $block->getActionsJson() ?> } } } } } </script> - - diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml index 86c8590bb4860..20988fad5ff35 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml @@ -13,7 +13,7 @@ provider="${ $.parentName }" sortOrder="10" class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -24,6 +24,7 @@ <item name="searchOptions" xsi:type="boolean">true</item> <item name="filterOptions" xsi:type="boolean">true</item> <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="validationUrl" xsi:type="url" path="media_gallery/asset/getSelected"/> <item name="levelsVisibility" xsi:type="number">1</item> </item> </argument> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml index 58881a8c9de6c..4abeb71679bde 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml @@ -13,7 +13,7 @@ provider="${ $.parentName }" sortOrder="10" class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -24,6 +24,7 @@ <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="validationUrl" xsi:type="url" path="media_gallery/asset/getSelected"/> <item name="levelsVisibility" xsi:type="number">1</item> </item> </argument> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml index 49206043725f9..b7307f9a74fae 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -16,43 +16,17 @@ </argument> <settings> <buttons> - <button name="add_selected"> - <param name="on_click" xsi:type="string">return false;</param> - <param name="sort_order" xsi:type="number">110</param> - <class>action-primary no-display media-gallery-add-selected</class> - <label translate="true">Add Selected</label> - </button> + <button name="add_selected" class="Magento\MediaGalleryUi\Ui\Component\Control\InsertAsstes"/> <button name="cancel"> <param name="on_click" xsi:type="string">MediabrowserUtility.closeDialog();</param> <param name="sort_order" xsi:type="number">1</param> <class>cancel action-quaternary</class> <label translate="true">Cancel</label> </button> - <button name="upload_image"> - <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> - <class>action-add scalable media-gallery-actions-buttons</class> - <param name="sort_order" xsi:type="number">20</param> - <label translate="true">Upload Image</label> - </button> - <button name="delete_folder"> - <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> - <param name="disabled" xsi:type="string">disabled</param> - <param name="sort_order" xsi:type="number">30</param> - <class>action-default scalable media-gallery-actions-buttons</class> - <label translate="true">Delete Folder</label> - </button> - <button name="create_folder"> - <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> - <param name="sort_order" xsi:type="number">10</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Create Folder</label> - </button> - <button name="delete_massaction"> - <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> - <param name="sort_order" xsi:type="number">50</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Images...</label> - </button> + <button name="upload_image" class="Magento\MediaGalleryUi\Ui\Component\Control\UploadAssets"/> + <button name="delete_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteFolder"/> + <button name="create_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\CreateFolder"/> + <button name="delete_massaction" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteAssets"/> </buttons> <spinner>media_gallery_columns</spinner> <deps> @@ -207,6 +181,7 @@ <container name="media_gallery_massactions" displayArea="sorting" sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Massactions\Massaction" component="Magento_MediaGalleryUi/js/grid/massaction/massactions" template="Magento_MediaGalleryUi/grid/massactions/count" > <argument name="data" xsi:type="array"> @@ -219,7 +194,7 @@ </container> </listingToolbar> <container name="media_gallery_directories" - class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + class="Magento\MediaGalleryUi\Ui\Component\DirectoryTree" template="Magento_MediaGalleryUi/grid/directories/directoryTree" component="Magento_MediaGalleryUi/js/directory/directoryTree"/> <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml index 2b7d9fde3b9ff..4634652e9cc19 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml @@ -13,7 +13,7 @@ provider="${ $.parentName }" sortOrder="10" class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -24,6 +24,7 @@ <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="validationUrl" xsi:type="url" path="media_gallery/asset/getSelected"/> <item name="levelsVisibility" xsi:type="number">1</item> </item> </argument> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml index 655178c104492..a53a46c61f75d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -20,30 +20,10 @@ <dep>standalone_media_gallery_listing.media_gallery_listing_data_source</dep> </deps> <buttons> - <button name="delete_folder"> - <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> - <param name="disabled" xsi:type="string">disabled</param> - <param name="sort_order" xsi:type="number">20</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Folder</label> - </button> - <button name="create_folder"> - <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> - <param name="sort_order" xsi:type="number">30</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Create Folder</label> - </button> - <button name="delete_massaction"> - <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> - <param name="sort_order" xsi:type="number">50</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Delete Images...</label> - </button> - <button name="upload_image"> - <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> - <class>action-default scalable add media-gallery-actions-buttons</class> - <label translate="true">Upload Image</label> - </button> + <button name="upload_image" class="Magento\MediaGalleryUi\Ui\Component\Control\UploadAssets"/> + <button name="delete_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteFolder"/> + <button name="create_folder" class="Magento\MediaGalleryUi\Ui\Component\Control\CreateFolder"/> + <button name="delete_massaction" class="Magento\MediaGalleryUi\Ui\Component\Control\DeleteAssets"/> </buttons> </settings> <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> @@ -194,6 +174,7 @@ <container name="media_gallery_massactions" displayArea="sorting" sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Massactions\Massaction" component="Magento_MediaGalleryUi/js/grid/massaction/massactions" template="Magento_MediaGalleryUi/grid/massactions/count" > <argument name="data" xsi:type="array"> @@ -206,7 +187,7 @@ </container> </listingToolbar> <container name="media_gallery_directories" - class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + class="Magento\MediaGalleryUi\Ui\Component\DirectoryTree" template="Magento_MediaGalleryUi/grid/directories/directoryTree" component="Magento_MediaGalleryUi/js/directory/directoryTree"/> <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less index fc8bd49126d8e..4b0d8f7dec89e 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -18,7 +18,7 @@ @color-media-gallery-buttons-border: #adadad; @color-media-gallery-buttons-text: #514943; @color-media-gallery-checkbox-background: #eee; - +@color-media-gallery-scrollbar-background: #fff; & when (@media-common = true) { .media-gallery-delete-image-action, @@ -99,6 +99,9 @@ .media-gallery-container { + .action-disabled { + opacity: .5; + } .masonry-image-grid .no-data-message-container, .masonry-image-grid .error-message-container { left: 50%; @@ -170,8 +173,9 @@ height: 30px; margin: 1px; padding-left: 6px; + padding-right: 10px; padding-top: 6px; - width: 100%; + width: max-content; } .jstree-default .jstree-clicked { @@ -272,8 +276,18 @@ } .media-directory-container { + &::-webkit-scrollbar { + background-color: @color-media-gallery-scrollbar-background; + } + &::-webkit-scrollbar-thumb { + background-color: @color-masonry-grey; + } float: left; + max-width: 50%; + overflow-x: scroll; + overflow-y: hidden; padding-right: 40px; + scrollbar-color: @color-masonry-grey @color-media-gallery-scrollbar-background; } .media-gallery-image-block { diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js index ed40674df20f0..28c021fe4728f 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -21,25 +21,24 @@ define([ * @param {String} deleteImageUrl */ deleteImageAction: function (recordsIds, imageDetailsUrl, deleteImageUrl) { - var confirmationContent = $t('%1 Are you sure you want to delete "%2" image(s)?') + var confirmationContent = $t('%1Are you sure you want to delete "%2" image(s)?') .replace('%2', Object.keys(recordsIds).length), deferred = $.Deferred(); - getDetails(imageDetailsUrl, recordsIds) - .then(function (imageDetails) { + getDetails(imageDetailsUrl, recordsIds).then(function (images) { confirmationContent = confirmationContent.replace( '%1', - this.getRecordRelatedContentMessage(imageDetails) + this.getRecordRelatedContentMessage(images) + ' ' ); }.bind(this)).fail(function () { - confirmationContent = confirmationContent.replace('%1', ''); - }).always(function () { - deleteImages(recordsIds, deleteImageUrl, confirmationContent).then(function (status) { - deferred.resolve(status); - }).fail(function (error) { - deferred.reject(error); - }); - }); + confirmationContent = confirmationContent.replace('%1', ''); + }).always(function () { + deleteImages(recordsIds, deleteImageUrl, confirmationContent).then(function (status) { + deferred.resolve(status); + }).fail(function (error) { + deferred.reject(error); + }); + }); return deferred.promise(); }, diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js index d7f756d8bbd90..5555baeabb66a 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -19,10 +19,12 @@ define([ return Component.extend({ defaults: { + allowedActions: [], directoryTreeSelector: '#media-gallery-directory-tree', deleteButtonSelector: '#delete_folder', createFolderButtonSelector: '#create_folder', messageDelay: 5, + selectedFolder: null, messagesName: 'media_gallery_listing.media_gallery_listing.messages', modules: { directoryTree: '${ $.parentName }.media_gallery_directories', @@ -47,51 +49,57 @@ define([ */ initEvents: function () { $(this.deleteButtonSelector).on('delete_folder', function () { - this.getConfirmationPopupDeleteFolder(); + this.deleteFolder(); }.bind(this)); $(this.createFolderButtonSelector).on('create_folder', function () { - this.getPrompt({ - title: $t('New Folder Name:'), - content: '', - actions: { - /** - * Confirm action - */ - confirm: function (folderName) { - createDirectory( - this.directoryTree().createDirectoryUrl, - [this.getNewFolderPath(folderName)] - ).then(function () { - this.directoryTree().reloadJsTree().then(function () { - $(this.directoryTree().directoryTreeSelector).on('loaded.jstree', function () { - this.directoryTree().locateNode(this.getNewFolderPath(folderName)); - }.bind(this)); - }.bind(this)); + this.createFolder(); + }.bind(this)); + }, - }.bind(this)).fail(function (error) { - uiAlert({ - content: error - }); + /** + * Show confirmation popup and create folder based on user input + */ + createFolder: function () { + this.getPrompt({ + title: $t('New Folder Name:'), + content: '', + actions: { + /** + * Confirm action + */ + confirm: function (folderName) { + createDirectory( + this.directoryTree().createDirectoryUrl, + [this.getNewFolderPath(folderName)] + ).then(function () { + this.directoryTree().reloadJsTree().then(function () { + $(this.directoryTree().directoryTreeSelector).on('loaded.jstree', function () { + this.directoryTree().locateNode(this.getNewFolderPath(folderName)); + }.bind(this)); + }.bind(this)); + }.bind(this)).fail(function (error) { + uiAlert({ + content: error }); - }.bind(this) - }, - buttons: [{ - text: $t('Cancel'), - class: 'action-secondary action-dismiss', - - /** - * Close modal - */ - click: function () { - this.closeModal(); - } - }, { - text: $t('Confirm'), - class: 'action-primary action-accept' - }] - }); - }.bind(this)); + }); + }.bind(this) + }, + buttons: [{ + text: $t('Cancel'), + class: 'action-secondary action-dismiss', + + /** + * Close modal + */ + click: function () { + this.closeModal(); + } + }, { + text: $t('Confirm'), + class: 'action-primary action-accept' + }] + }); }, /** @@ -101,11 +109,11 @@ define([ * @returns {String} */ getNewFolderPath: function (folderName) { - var selectedFolder = _.isUndefined(this.selectedFolder()) || - _.isNull(this.selectedFolder()) ? '/' : this.selectedFolder(), - folderToCreate = selectedFolder !== '/' ? selectedFolder + '/' + folderName : folderName; + if (_.isUndefined(this.selectedFolder()) || _.isNull(this.selectedFolder())) { + return folderName; + } - return folderToCreate; + return this.selectedFolder() + '/' + folderName; }, /** @@ -136,7 +144,7 @@ define([ /** * Confirmation popup for delete folder action. */ - getConfirmationPopupDeleteFolder: function () { + deleteFolder: function () { confirm({ title: $t('Are you sure you want to delete this folder?'), modalClass: 'delete-folder-confirmation-popup', @@ -179,6 +187,10 @@ define([ * @param {String} folderId */ setActive: function (folderId) { + if (!this.allowedActions.includes('delete_folder')) { + return; + } + this.selectedFolder(folderId); $(this.deleteButtonSelector).removeAttr('disabled').removeClass('disabled'); } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js index decc337e1b83c..84f253da826a8 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -17,20 +17,24 @@ define([ return Component.extend({ defaults: { + allowedActions: [], filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', directoryTreeSelector: '#media-gallery-directory-tree', getDirectoryTreeUrl: 'media_gallery/directories/gettree', + createDirectoryUrl: 'media_gallery/directories/create', + deleteDirectoryUrl: 'media_gallery/directories/delete', jsTreeReloaded: null, modules: { directories: '${ $.name }_directories', filterChips: '${ $.filterChipsProvider }' }, listens: { - '${ $.provider }:params.filters.path': 'clearFiltersHandle' + '${ $.provider }:params.filters.path': 'updateSelectedDirectory' }, viewConfig: [{ component: 'Magento_MediaGalleryUi/js/directory/directories', - name: '${ $.name }_directories' + name: '${ $.name }_directories', + allowedActions: '${ $.allowedActions }' }] }, @@ -49,7 +53,8 @@ define([ this.renderDirectoryTree().then(function () { this.initEvents(); }.bind(this)); - }.bind(this)); + }.bind(this) + ); return this; }, @@ -58,7 +63,6 @@ define([ * Render directory tree component. */ renderDirectoryTree: function () { - return this.getJsonTree().then(function (data) { this.createFolderIfNotExists(data).then(function (isFolderCreated) { if (isFolderCreated) { @@ -87,37 +91,37 @@ define([ * @param {Array} directories */ createFolderIfNotExists: function (directories) { - var isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), - currentTreePath = isMediaBrowser ? window.MediabrowserUtility.pathId : null, + var requestedDirectory = this.getRequestedDirectory(), deferred = $.Deferred(), - decodedPath, pathArray; - if (currentTreePath) { - decodedPath = Base64.idDecode(currentTreePath); - - if (!this.isDirectoryExist(directories[0], decodedPath)) { - pathArray = this.convertPathToPathsArray(decodedPath); + if (_.isNull(requestedDirectory)) { + deferred.resolve(false); - $.each(pathArray, function (i, val) { - if (this.isDirectoryExist(directories[0], val)) { - pathArray.splice(i, 1); - } - }.bind(this)); + return deferred.promise(); + } - createDirectory( - this.createDirectoryUrl, - pathArray - ).then(function () { - deferred.resolve(true); - }); - } else { - deferred.resolve(false); - } - } else { + if (this.isDirectoryExist(directories[0], requestedDirectory)) { deferred.resolve(false); + + return deferred.promise(); } + pathArray = this.convertPathToPathsArray(requestedDirectory); + + $.each(pathArray, function (i, val) { + if (this.isDirectoryExist(directories[0], val)) { + pathArray.splice(i, 1); + } + }.bind(this)); + + createDirectory( + this.createDirectoryUrl, + pathArray + ).then(function () { + deferred.resolve(true); + }); + return deferred.promise(); }, @@ -199,7 +203,7 @@ define([ /** * Remove ability to multiple select on nodes */ - overrideMultiselectBehavior: function () { + disableMultiselectBehavior: function () { $.jstree.defaults.ui['select_range_modifier'] = false; $.jstree.defaults.ui['select_multiple_modifier'] = false; }, @@ -208,8 +212,8 @@ define([ * Handle jstree events */ initEvents: function () { - this.firejsTreeEvents(); - this.overrideMultiselectBehavior(); + this.initJsTreeEvents(); + this.disableMultiselectBehavior(); $(window).on('reload.MediaGallery', function () { this.getJsonTree().then(function (data) { @@ -217,10 +221,10 @@ define([ if (isCreated) { this.renderDirectoryTree().then(function () { this.setJsTreeReloaded(true); - this.firejsTreeEvents(); + this.initJsTreeEvents(); }.bind(this)); } else { - this.checkChipFiltersState(); + this.updateSelectedDirectory(); } }.bind(this)); }.bind(this)); @@ -230,30 +234,33 @@ define([ /** * Fire event for jstree component */ - firejsTreeEvents: function () { + initJsTreeEvents: function () { $(this.directoryTreeSelector).on('select_node.jstree', function (element, data) { - var path = $(data.rslt.obj).data('path'); - - this.setActiveNodeFilter(path); + this.setActiveNodeFilter($(data.rslt.obj).data('path')); this.setJsTreeReloaded(false); }.bind(this)); $(this.directoryTreeSelector).on('loaded.jstree', function () { - this.checkChipFiltersState(); + this.updateSelectedDirectory(); }.bind(this)); - }, /** * Verify directory filter on init event, select folder per directory filter state */ - checkChipFiltersState: function () { + updateSelectedDirectory: function () { var currentFilterPath = this.filterChips().filters.path, - isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), + requestedDirectory = this.getRequestedDirectory(), currentTreePath; - currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : - Base64.idDecode(window.MediabrowserUtility.pathId); + if (_.isUndefined(currentFilterPath)) { + this.clearFiltersHandle(); + + return; + } + + currentTreePath = this.isFilterApplied(currentFilterPath) || _.isNull(requestedDirectory) ? + currentFilterPath : requestedDirectory; if (this.folderExistsInTree(currentTreePath)) { this.locateNode(currentTreePath); @@ -275,14 +282,23 @@ define([ return false; }, + /** + * Get requested directory from MediabrowserUtility + * + * @returns {String|null} + */ + getRequestedDirectory: function () { + return !_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '' ? + Base64.idDecode(window.MediabrowserUtility.pathId) : null; + }, + /** * Check if need to select directory by filters state * * @param {String} currentFilterPath */ - isFiltersApplied: function (currentFilterPath) { - return !_.isUndefined(currentFilterPath) && currentFilterPath !== '' && - currentFilterPath !== 'wysiwyg' && currentFilterPath !== 'catalog/category'; + isFilterApplied: function (currentFilterPath) { + return !_.isUndefined(currentFilterPath) && currentFilterPath !== ''; }, /** @@ -291,9 +307,7 @@ define([ * @param {String} path */ locateNode: function (path) { - var selectedId = $(this.directoryTreeSelector).jstree('get_selected').attr('id'); - - if (path === selectedId) { + if (path === $(this.directoryTreeSelector).jstree('get_selected').attr('id')) { return; } path = path.replace(/\//g, '\\/'); @@ -303,14 +317,12 @@ define([ }, /** - * Listener to clear filters event + * Clear filters */ clearFiltersHandle: function () { - if (_.isUndefined(this.filterChips().filters.path)) { - $(this.directoryTreeSelector).jstree('deselect_all'); - this.activeNode(null); - this.directories().setInActive(); - } + $(this.directoryTreeSelector).jstree('deselect_all'); + this.activeNode(null); + this.directories().setInActive(); }, /** @@ -319,7 +331,6 @@ define([ * @param {String} nodePath */ setActiveNodeFilter: function (nodePath) { - if (this.activeNode() === nodePath && !this.jsTreeReloaded) { this.selectStorageRoot(); } else { @@ -341,14 +352,13 @@ define([ this.filterChips().set('applied', filters); this.activeNode(null); this.waitForCondition( - function () { - return _.isUndefined(this.directories()); - }.bind(this), function () { - this.directories().setInActive(); - }.bind(this) - ); - + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setInActive(); + }.bind(this) + ); }, /** @@ -372,8 +382,8 @@ define([ }, /** - * Remove active node from directory tree, and select next - */ + * Remove active node from directory tree, and select next + */ removeNode: function () { $(this.directoryTreeSelector).jstree('remove'); }, @@ -390,7 +400,6 @@ define([ filters = $.extend(true, filters, applied); filters.path = path; this.filterChips().set('applied', filters); - }, /** diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js index bf852d0ddae68..974e22e23737c 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -13,10 +13,13 @@ define([ return Column.extend({ defaults: { bodyTmpl: 'Magento_MediaGalleryUi/grid/columns/image', + messageContentSelector: 'ul.messages', + mediaGalleryContainerSelector: '.media-gallery-container', deleteImageUrl: 'media_gallery/image/delete', addSelectedBtnSelector: '#add_selected', deleteSelectedBtnSelector: '#delete_selected', selected: null, + allowedActions: [], fields: { id: 'id', url: 'url', @@ -39,7 +42,8 @@ define([ { component: 'Magento_MediaGalleryUi/js/grid/columns/image/actions', name: '${ $.name }_actions', - imageModelName: '${ $.name }' + imageModelName: '${ $.name }', + allowedActions: '${ $.allowedActions }' } ] }, @@ -222,8 +226,15 @@ define([ toggleAddSelectedButton: function () { if (this.selected() === null) { this.hideAddSelectedAndDeleteButon(); - } else { + + return; + } + + if (this.allowedActions.includes('insert')) { $(this.addSelectedBtnSelector).removeClass('no-display'); + } + + if (this.allowedActions.includes('delete')) { $(this.deleteSelectedBtnSelector).removeClass('no-display'); } }, @@ -270,6 +281,7 @@ define([ */ addMessage: function (code, message) { this.messages().add(code, message); + this.scrollToMessageContent(); this.messages().scheduleCleanup(); }, @@ -284,6 +296,20 @@ define([ !this.massaction().massActionMode()) { this.deselectImage(); } + }, + + /** + * Scroll to the top of media gallery page + */ + scrollToMessageContent: function () { + var scrollTargetElement = $(this.messageContentSelector), + scrollTargetContainer = $(this.mediaGalleryContainerSelector); + + scrollTargetContainer.find(scrollTargetElement).get(0).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest' + }); } }); }); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js index 38743c8d83d3b..76e051072285a 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -8,7 +8,8 @@ define([ 'uiComponent', 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', - 'mage/translate' + 'mage/translate', + 'Magento_Ui/js/lib/view/utils/async' ], function ($, _, Component, deleteImageWithDetailConfirmation, image, $t) { 'use strict'; @@ -17,20 +18,24 @@ define([ template: 'Magento_MediaGalleryUi/grid/columns/image/actions', mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + allowedActions: [], actionsList: [ { name: 'image-details', title: $t('View Details'), + classes: 'action-menu-item', handler: 'viewImageDetails' }, { name: 'edit', title: $t('Edit'), + classes: 'action-menu-item', handler: 'editImageDetails' }, { name: 'delete', title: $t('Delete'), + classes: 'action-menu-item media-gallery-delete-assets', handler: 'deleteImageAction' } ], @@ -50,6 +55,16 @@ define([ this._super(); this.initEvents(); + this.actionsList = this.actionsList.filter(function (item) { + return this.allowedActions.includes(item.name); + }.bind(this)); + + if (!this.allowedActions.includes('delete')) { + $.async('.media-gallery-delete-assets', function () { + $('.media-gallery-delete-assets').unbind('click').addClass('action-disabled'); + }); + } + return this; }, diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js index 4f09854005f23..a20239fb1165e 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js @@ -16,6 +16,7 @@ define([ return Component.extend({ defaults: { + allowedActions: [], deleteButtonSelector: '#delete_selected_massaction', deleteImagesSelector: '#delete_massaction', mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', @@ -106,6 +107,10 @@ define([ * If images records less than one, disable "delete images" button */ checkButtonVisibility: function () { + if (!this.allowedActions.includes('delete_assets')) { + return; + } + if (this.imageItems.length < 1) { $(this.deleteImagesSelector).addClass('disabled'); } else { @@ -141,10 +146,8 @@ define([ if (response.status === 'canceled') { return; } - this.imageModel().selected({}); - this.massActionMode(false); - this.switchMode(); - }.bind(this)); + $(window).trigger('terminateMassAction.MediaGallery'); + }); } }.bind(this)); } diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html index 042e119b9f40e..72447196cea55 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html @@ -7,9 +7,9 @@ <each args="{ data: actionsList, as: 'action' }"> <li> - <a class="action-menu-item" href="" text="action.title" + <a href="#" text="action.title" click="$parent[action.handler].bind($parent, $row())" - attr="{'data-action': 'item-' + action.name}"> + attr="{'data-action': 'item-' + action.name, class: action.classes}"> </a> </li> -</each> \ No newline at end of file +</each> diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json index f8d5ef11058c1..d577f50523f13 100644 --- a/app/code/Magento/MediaGalleryUiApi/composer.json +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -5,6 +5,9 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*" }, + "suggest": { + "magento/module-cms": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", diff --git a/app/code/Magento/MediaGalleryUiApi/etc/acl.xml b/app/code/Magento/MediaGalleryUiApi/etc/acl.xml new file mode 100644 index 0000000000000..c496c57d51322 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/etc/acl.xml @@ -0,0 +1,27 @@ +<?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:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Backend::content"> + <resource id="Magento_Backend::content_elements"> + <resource id="Magento_Cms::media_gallery" title="Media Gallery" translate="title"> + <resource id="Magento_MediaGalleryUiApi::insert_assets" title="Insert assets into the content" translate="title" sortOrder="40"/> + <resource id="Magento_MediaGalleryUiApi::upload_assets" title="Upload assets" translate="title" sortOrder="50"/> + <resource id="Magento_MediaGalleryUiApi::edit_assets" title="Edit asset details" translate="title" sortOrder="60"/> + <resource id="Magento_MediaGalleryUiApi::delete_assets" title="Delete assets" translate="title" sortOrder="70"/> + <resource id="Magento_MediaGalleryUiApi::create_folder" title="Create folder" translate="title" sortOrder="80"/> + <resource id="Magento_MediaGalleryUiApi::delete_folder" title="Delete folder" translate="title" sortOrder="90"/> + </resource> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/MessageQueue/etc/di.xml b/app/code/Magento/MessageQueue/etc/di.xml index f60eb5fbc20df..b283280dc4580 100644 --- a/app/code/Magento/MessageQueue/etc/di.xml +++ b/app/code/Magento/MessageQueue/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\MessageQueue\ConfigInterface" type="Magento\Framework\MessageQueue\Config\Proxy" /> <preference for="Magento\Framework\MessageQueue\LockInterface" type="Magento\Framework\MessageQueue\Lock" /> <preference for="Magento\Framework\MessageQueue\Lock\WriterInterface" type="Magento\MessageQueue\Model\ResourceModel\Lock" /> <preference for="Magento\Framework\MessageQueue\Lock\ReaderInterface" type="Magento\MessageQueue\Model\ResourceModel\Lock" /> diff --git a/app/code/Magento/Multishipping/Controller/Checkout.php b/app/code/Magento/Multishipping/Controller/Checkout.php index 92417c7cb3a18..e4711d8766c8b 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout.php +++ b/app/code/Magento/Multishipping/Controller/Checkout.php @@ -3,88 +3,79 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Controller; +use Magento\Checkout\Controller\Action; +use Magento\Checkout\Controller\Express\RedirectLoginInterface; +use Magento\Checkout\Model\Session as ModelSession; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Action\Context; use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\Exception\StateException; +use Magento\Multishipping\Helper\Url; +use Magento\Multishipping\Model\Checkout\Type\Multishipping; +use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; /** * Multishipping checkout controller + * * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -abstract class Checkout extends \Magento\Checkout\Controller\Action implements - \Magento\Checkout\Controller\Express\RedirectLoginInterface +abstract class Checkout extends Action implements RedirectLoginInterface { - /** - * Constructor - * - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Customer\Model\Session $customerSession - * @param CustomerRepositoryInterface $customerRepository - * @param AccountManagementInterface $accountManagement - */ - public function __construct( - \Magento\Framework\App\Action\Context $context, - \Magento\Customer\Model\Session $customerSession, - CustomerRepositoryInterface $customerRepository, - AccountManagementInterface $accountManagement - ) { - parent::__construct( - $context, - $customerSession, - $customerRepository, - $accountManagement - ); - } /** * Retrieve checkout model * - * @return \Magento\Multishipping\Model\Checkout\Type\Multishipping + * @return Multishipping */ protected function _getCheckout() { - return $this->_objectManager->get(\Magento\Multishipping\Model\Checkout\Type\Multishipping::class); + return $this->_objectManager->get(Multishipping::class); } /** * Retrieve checkout state model * - * @return \Magento\Multishipping\Model\Checkout\Type\Multishipping\State + * @return State */ protected function _getState() { - return $this->_objectManager->get(\Magento\Multishipping\Model\Checkout\Type\Multishipping\State::class); + return $this->_objectManager->get(State::class); } /** * Retrieve checkout url helper * - * @return \Magento\Multishipping\Helper\Url + * @return Url */ protected function _getHelper() { - return $this->_objectManager->get(\Magento\Multishipping\Helper\Url::class); + return $this->_objectManager->get(Url::class); } /** * Retrieve checkout session * - * @return \Magento\Checkout\Model\Session + * @return ModelSession */ protected function _getCheckoutSession() { - return $this->_objectManager->get(\Magento\Checkout\Model\Session::class); + return $this->_objectManager->get(ModelSession::class); } /** * Dispatch request * * @param RequestInterface $request - * @return \Magento\Framework\App\ResponseInterface + * @return ResponseInterface * @throws \Magento\Framework\Exception\NotFoundException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) @@ -104,7 +95,7 @@ public function dispatch(RequestInterface $request) */ if ($action == 'index') { $checkoutSessionQuote->setIsMultiShipping(true); - $this->_getCheckoutSession()->setCheckoutState(\Magento\Checkout\Model\Session::CHECKOUT_STATE_BEGIN); + $this->_getCheckoutSession()->setCheckoutState(ModelSession::CHECKOUT_STATE_BEGIN); } elseif (!$checkoutSessionQuote->getIsMultiShipping() && !in_array( $action, ['login', 'register', 'success'] @@ -116,7 +107,7 @@ public function dispatch(RequestInterface $request) } if (!in_array($action, ['login', 'register'])) { - $customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + $customerSession = $this->_objectManager->get(Session::class); if (!$customerSession->authenticate($this->_getHelper()->getMSLoginUrl())) { $this->_actionFlag->set('', self::FLAG_NO_DISPATCH, true); } @@ -125,7 +116,7 @@ public function dispatch(RequestInterface $request) \Magento\Multishipping\Helper\Data::class )->isMultishippingCheckoutAvailable()) { $error = $this->_getCheckout()->getMinimumAmountError(); - $this->messageManager->addError($error); + $this->messageManager->addErrorMessage($error); $this->getResponse()->setRedirect($this->_getHelper()->getCartUrl()); $this->_actionFlag->set('', self::FLAG_NO_DISPATCH, true); return parent::dispatch($request); @@ -133,7 +124,7 @@ public function dispatch(RequestInterface $request) } $result = $this->_preDispatchValidateCustomer(); - if ($result instanceof \Magento\Framework\Controller\ResultInterface) { + if ($result instanceof ResultInterface) { return $result; } @@ -180,7 +171,7 @@ protected function _validateMinimumAmount() { if (!$this->_getCheckout()->validateMinimumAmount()) { $error = $this->_getCheckout()->getMinimumAmountError(); - $this->messageManager->addError($error); + $this->messageManager->addErrorMessage($error); $this->_forward('backToAddresses'); return false; } diff --git a/app/code/Magento/Multishipping/Controller/Checkout/AddressesPost.php b/app/code/Magento/Multishipping/Controller/Checkout/AddressesPost.php index 060a1bdd5ac4e..2a4cb47b421c0 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/AddressesPost.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/AddressesPost.php @@ -4,11 +4,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Controller\Checkout; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Multishipping\Controller\Checkout; use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; -class AddressesPost extends \Magento\Multishipping\Controller\Checkout +class AddressesPost extends Checkout implements HttpPostActionInterface { /** * Multishipping checkout process posted addresses @@ -36,7 +40,7 @@ public function execute() $this->_getCheckout()->setShippingItemsInformation($shipToInfo); } } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*/addresses'); } catch (\Exception $e) { $this->messageManager->addException($e, __('Data saving problem')); diff --git a/app/code/Magento/Multishipping/Controller/Checkout/Overview.php b/app/code/Magento/Multishipping/Controller/Checkout/Overview.php index d97226a393c25..623ced14c2fa9 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/Overview.php @@ -4,14 +4,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Controller\Checkout; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Multishipping\Controller\Checkout; use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; use Magento\Payment\Model\Method\AbstractMethod; use Psr\Log\LoggerInterface; -class Overview extends \Magento\Multishipping\Controller\Checkout +class Overview extends Checkout implements HttpPostActionInterface, HttpGetActionInterface { /** * Multishipping checkout place order page @@ -42,7 +47,7 @@ public function execute() $this->_view->loadLayout(); $this->_view->renderLayout(); } catch (LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*/billing'); } catch (\Exception $e) { $this->_objectManager->get(LoggerInterface::class)->critical($e); diff --git a/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php b/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php index f05a7f43b8118..762b0f5cca59c 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php @@ -3,33 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Controller\Checkout; -use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; +use Magento\Checkout\Api\AgreementsValidatorInterface; use Magento\Customer\Api\AccountManagementInterface; 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\Data\Form\FormKey\Validator; use Magento\Framework\Exception\PaymentException; use Magento\Framework\Session\SessionManagerInterface; +use Magento\Multishipping\Controller\Checkout; +use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; +use Psr\Log\LoggerInterface; /** - * Class OverviewPost - * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class OverviewPost extends \Magento\Multishipping\Controller\Checkout +class OverviewPost extends Checkout implements HttpPostActionInterface { /** - * @var \Magento\Framework\Data\Form\FormKey\Validator + * @var Validator */ protected $formKeyValidator; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ protected $logger; /** - * @var \Magento\Checkout\Api\AgreementsValidatorInterface + * @var AgreementsValidatorInterface */ protected $agreementsValidator; @@ -39,23 +46,23 @@ class OverviewPost extends \Magento\Multishipping\Controller\Checkout private $session; /** - * @param \Magento\Framework\App\Action\Context $context - * @param \Magento\Customer\Model\Session $customerSession + * @param Context $context + * @param Session $customerSession * @param CustomerRepositoryInterface $customerRepository * @param AccountManagementInterface $accountManagement - * @param \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator + * @param Validator $formKeyValidator + * @param LoggerInterface $logger + * @param AgreementsValidatorInterface $agreementValidator * @param SessionManagerInterface $session */ public function __construct( - \Magento\Framework\App\Action\Context $context, - \Magento\Customer\Model\Session $customerSession, + Context $context, + Session $customerSession, CustomerRepositoryInterface $customerRepository, AccountManagementInterface $accountManagement, - \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator, - \Psr\Log\LoggerInterface $logger, - \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator, + Validator $formKeyValidator, + LoggerInterface $logger, + AgreementsValidatorInterface $agreementValidator, SessionManagerInterface $session ) { $this->formKeyValidator = $formKeyValidator; @@ -89,7 +96,7 @@ public function execute() try { if (!$this->agreementsValidator->isValid(array_keys($this->getRequest()->getPost('agreement', [])))) { - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __('Please agree to all Terms and Conditions before placing the order.') ); $this->_redirect('*/*/billing'); @@ -119,7 +126,7 @@ public function execute() } catch (PaymentException $e) { $message = $e->getMessage(); if (!empty($message)) { - $this->messageManager->addError($message); + $this->messageManager->addErrorMessage($message); } $this->_redirect('*/*/billing'); } catch (\Magento\Checkout\Exception $e) { @@ -131,7 +138,7 @@ public function execute() 'multi-shipping' ); $this->_getCheckout()->getCheckoutSession()->clearQuote(); - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/cart'); } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->_objectManager->get( @@ -141,7 +148,7 @@ public function execute() $e->getMessage(), 'multi-shipping' ); - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*/billing'); } catch (\Exception $e) { $this->logger->critical($e); @@ -156,7 +163,7 @@ public function execute() } catch (\Exception $e) { $this->logger->error($e->getMessage()); } - $this->messageManager->addError(__('Order place error')); + $this->messageManager->addErrorMessage(__('Order place error')); $this->_redirect('*/*/billing'); } } diff --git a/app/code/Magento/Multishipping/Controller/Checkout/ShippingPost.php b/app/code/Magento/Multishipping/Controller/Checkout/ShippingPost.php index 1e1d3dbace623..d8ab9faa24a36 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/ShippingPost.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/ShippingPost.php @@ -4,13 +4,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Multishipping\Controller\Checkout; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Multishipping\Controller\Checkout; use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; -class ShippingPost extends \Magento\Multishipping\Controller\Checkout +class ShippingPost extends Checkout implements HttpPostActionInterface { /** + * Shipping action + * * @return void */ public function execute() @@ -26,7 +32,7 @@ public function execute() $this->_getState()->setCompleteStep(State::STEP_SHIPPING); $this->_redirect('*/*/billing'); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $this->_redirect('*/*/shipping'); } } diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php index 49212202b5f62..8845395be406e 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping.php @@ -695,7 +695,7 @@ protected function _prepareOrder(\Magento\Quote\Model\Quote\Address $address) ); $shippingMethodCode = $address->getShippingMethod(); - if (isset($shippingMethodCode) && !empty($shippingMethodCode)) { + if ($shippingMethodCode) { $rate = $address->getShippingRateByCode($shippingMethodCode); $shippingPrice = $rate->getPrice(); } else { @@ -975,7 +975,8 @@ public function getMinimumAmountError() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } - return $error; + + return __($error); } /** diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontRemoveProductOnCheckoutActionGroup.xml similarity index 61% rename from app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml rename to app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontRemoveProductOnCheckoutActionGroup.xml index 0f87ee90b7ce0..af0f3e2d597b8 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpentCmsBlockActionGroup.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/ActionGroup/StorefrontRemoveProductOnCheckoutActionGroup.xml @@ -5,12 +5,14 @@ * See COPYING.txt for license details. */ --> + <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminOpenCmsBlockActionGroup"> + <actionGroup name="StorefrontRemoveProductOnCheckoutActionGroup"> <arguments> - <argument name="block_id" type="string"/> + <argument name="itemNumber" type="string" defaultValue="1"/> </arguments> - <amOnPage url="{{AdminEditBlockPage.url(block_id)}}" stepKey="openEditCmsBlock"/> + + <click selector="{{MultishippingSection.removeItemButton(itemNumber)}}" stepKey="removeItem"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml index db037d50f7dc6..9c89ffa3cd405 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Section/MultishippingSection/MultishippingSection.xml @@ -14,5 +14,6 @@ <element name="shippingAddressSelector" type="select" selector="//tr[position()={{addressPosition}}]//td[@data-th='Send To']//select" parameterized="true"/> <element name="shippingAddressOptions" type="select" selector="#multiship-addresses-table tbody tr:nth-of-type({{addressPosition}}) .col.address select option:nth-of-type({{optionIndex}})" parameterized="true"/> <element name="selectShippingAddress" type="select" selector="(//table[@id='multiship-addresses-table'] //div[@class='field address'] //select)[{{sequenceNumber}}]" parameterized="true"/> + <element name="removeItemButton" type="button" selector="//a[contains(@title, 'Remove Item')][{{var}}]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.xml new file mode 100644 index 0000000000000..632950120474d --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontCheckoutWithWithVirtualProductTest.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="StorefrontCheckoutWithWithVirtualProductTest"> + <annotations> + <features value="Multishipping"/> + <stories value="Multiple Shipping"/> + <title value="Check error when cart contains virtual product"/> + <description value="Check error when cart contains only virtual product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-36921"/> + <group value="Multishipment"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="firstProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="VirtualProduct" stepKey="virtualProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Customer_US_UK_DE" stepKey="createCustomerWithMultipleAddresses"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <deleteData createDataKey="firstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="virtualProduct" stepKey="deleteVirtualProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createCustomerWithMultipleAddresses" stepKey="deleteCustomer"/> + </after> + <!-- Login to the Storefront as created customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomerWithMultipleAddresses$$"/> + </actionGroup> + <!-- Open the simple product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToFirstProductPage"> + <argument name="productUrl" value="$$firstProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Add the simple product to the Shopping Cart --> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPageActionGroup" stepKey="addFirstProductToCart"> + <argument name="productName" value="$$firstProduct.name$$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <!-- Open the virtual product page --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="goToVirtualProductPage"> + <argument name="productUrl" value="$$virtualProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <!-- Add the virtual product to the Shopping Cart --> + <actionGroup ref="AddProductWithQtyToCartFromStorefrontProductPageActionGroup" stepKey="addVirtualProductToCart"> + <argument name="productName" value="$$virtualProduct.name$$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <!-- Go to Cart --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <!-- Check Out with Multiple Addresses --> + <actionGroup ref="StorefrontCheckoutWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <!-- Remove simple product from cart --> + <actionGroup ref="StorefrontRemoveProductOnCheckoutActionGroup" stepKey="removeFirstProductFromCart"/> + <!-- Assert error message on checkout --> + <actionGroup ref="StorefrontAssertCheckoutErrorMessageActionGroup" stepKey="assertErrorMessage"> + <argument name="message" value="The current cart does not match multi shipping criteria, please review or contact the store administrator"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml index 494259e0ead9d..fe33078755ac4 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml @@ -33,8 +33,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$category.name$$)}}" stepKey="goToCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="moveMouseOverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="clickAddToCartButton"/> - <waitForPageLoad stepKey="waitForAddToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="clickAddToCartButton"/> <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForAddedToCartSuccessMessage"/> <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$product.name$$ to your shopping cart." stepKey="seeAddedToCartSuccessMessage"/> <see selector="{{StorefrontMinicartSection.quantity}}" userInput="1" stepKey="seeCartQuantity"/> diff --git a/app/code/Magento/Multishipping/etc/config.xml b/app/code/Magento/Multishipping/etc/config.xml index aee4199ed4757..0df8742016aed 100644 --- a/app/code/Magento/Multishipping/etc/config.xml +++ b/app/code/Magento/Multishipping/etc/config.xml @@ -13,5 +13,10 @@ <checkout_multiple_maximum_qty>100</checkout_multiple_maximum_qty> </options> </multishipping> + <sales> + <minimum_order> + <multi_address_error_message>The current cart does not match multi shipping criteria, please review or contact the store administrator</multi_address_error_message> + </minimum_order> + </sales> </default> </config> diff --git a/app/code/Magento/Multishipping/i18n/en_US.csv b/app/code/Magento/Multishipping/i18n/en_US.csv index 0c248bdcc1af3..430b16b8cc237 100644 --- a/app/code/Magento/Multishipping/i18n/en_US.csv +++ b/app/code/Magento/Multishipping/i18n/en_US.csv @@ -92,3 +92,4 @@ Options,Options "Error:","Error:" "We are unable to process your request. Please, try again later.","We are unable to process your request. Please, try again later." "Quote address for failed order ID "%1" not found.","Quote address for failed order ID "%1" not found." +"The current cart does not match multi shipping criteria, please review or contact the store administrator","The current cart does not match multi shipping criteria, please review or contact the store administrator" diff --git a/app/code/Magento/NewRelicReporting/Model/Observer/CheckConfig.php b/app/code/Magento/NewRelicReporting/Model/Observer/CheckConfig.php index f7eb658f40784..00060c8f94ca5 100644 --- a/app/code/Magento/NewRelicReporting/Model/Observer/CheckConfig.php +++ b/app/code/Magento/NewRelicReporting/Model/Observer/CheckConfig.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\NewRelicReporting\Model\Observer; use Magento\Framework\Event\Observer; @@ -11,9 +13,6 @@ use Magento\Framework\Message\ManagerInterface; use Magento\NewRelicReporting\Model\NewRelicWrapper; -/** - * Class CheckConfig - */ class CheckConfig implements ObserverInterface { /** @@ -58,9 +57,9 @@ public function execute(Observer $observer) if ($this->config->isNewRelicEnabled()) { if (!$this->newRelicWrapper->isExtensionInstalled()) { $this->config->disableModule(); - $this->messageManager->addError( + $this->messageManager->addErrorMessage( __( - 'The New Relic integration requires the newrelic-php5 agent, which is not installed. More + 'The New Relic integration requires the newrelic-php5 agent, which is not installed. More information on installing the agent is available <a target="_blank" href="%1">here</a>.', 'https://docs.newrelic.com/docs/agents/php-agent/installation/php-agent-installation-overview' ), diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Observer/CheckConfigTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Observer/CheckConfigTest.php index 6bb1ee7214609..f64acdc71d2a8 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Observer/CheckConfigTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Observer/CheckConfigTest.php @@ -125,7 +125,7 @@ public function testCheckConfig() $this->config->expects($this->once()) ->method('disableModule'); $this->messageManager->expects($this->once()) - ->method('addError'); + ->method('addErrorMessage'); $this->model->execute($eventObserver); } diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php index 2dbe10bf1bdc9..4f93e3e4f73a3 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Queue/Save.php @@ -88,7 +88,7 @@ public function execute() $this->_redirect('*/*'); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); $id = $this->getRequest()->getParam('id'); if ($id) { $this->_redirect('*/*/edit', ['id' => $id]); diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php index cdef44b2da757..e8ec57f7a153e 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassDelete.php @@ -46,7 +46,7 @@ public function execute() { $subscribersIds = $this->getRequest()->getParam('subscriber'); if (!is_array($subscribersIds)) { - $this->messageManager->addError(__('Please select one or more subscribers.')); + $this->messageManager->addErrorMessage(__('Please select one or more subscribers.')); } else { try { foreach ($subscribersIds as $subscriberId) { @@ -57,7 +57,7 @@ public function execute() } $this->messageManager->addSuccess(__('Total of %1 record(s) were deleted.', count($subscribersIds))); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php index b61494f795905..5e0890215c815 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Subscriber/MassUnsubscribe.php @@ -4,21 +4,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Newsletter\Controller\Adminhtml\Subscriber; -use Magento\Newsletter\Controller\Adminhtml\Subscriber; use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\Response\Http\FileFactory; +use Magento\Newsletter\Controller\Adminhtml\Subscriber; use Magento\Newsletter\Model\SubscriberFactory; -use Magento\Framework\App\ObjectManager; -class MassUnsubscribe extends Subscriber +class MassUnsubscribe extends Subscriber implements HttpPostActionInterface { /** * @var SubscriberFactory */ private $subscriberFactory; - + /** * @param Context $context * @param FileFactory $fileFactory @@ -32,17 +35,17 @@ public function __construct( $this->subscriberFactory = $subscriberFactory ?: ObjectManager::getInstance()->get(SubscriberFactory::class); parent::__construct($context, $fileFactory); } - + /** * Unsubscribe one or more subscribers action * * @return void */ - public function execute() + public function execute(): void { $subscribersIds = $this->getRequest()->getParam('subscriber'); if (!is_array($subscribersIds)) { - $this->messageManager->addError(__('Please select one or more subscribers.')); + $this->messageManager->addErrorMessage(__('Please select one or more subscribers.')); } else { try { foreach ($subscribersIds as $subscriberId) { @@ -53,7 +56,7 @@ public function execute() } $this->messageManager->addSuccess(__('A total of %1 record(s) were updated.', count($subscribersIds))); } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } } diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php index d327d44feceb8..c8352a028fe2d 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Delete.php @@ -4,9 +4,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Newsletter\Controller\Adminhtml\Template; -class Delete extends \Magento\Newsletter\Controller\Adminhtml\Template +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Newsletter\Controller\Adminhtml\Template; + +class Delete extends Template implements HttpGetActionInterface, HttpPostActionInterface { /** * Delete newsletter Template @@ -26,7 +32,7 @@ public function execute() $this->messageManager->addSuccess(__('The newsletter template has been deleted.')); $this->_getSession()->setFormData(false); } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); + $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addException($e, __('We can\'t delete this template right now.')); } diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php index dc4d50c22b162..cf4aad2059a01 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php @@ -69,7 +69,7 @@ public function execute() $this->_redirect('*/template'); return; } catch (LocalizedException $e) { - $this->messageManager->addError(nl2br($e->getMessage())); + $this->messageManager->addErrorMessage(nl2br($e->getMessage())); $this->_getSession()->setData('newsletter_template_form_data', $this->getRequest()->getParams()); } catch (\Exception $e) { $this->messageManager->addException($e, __('Something went wrong while saving this template.')); diff --git a/app/code/Magento/Newsletter/Controller/Manage/Save.php b/app/code/Magento/Newsletter/Controller/Manage/Save.php index 01012e39a992a..b70c84a6d1099 100644 --- a/app/code/Magento/Newsletter/Controller/Manage/Save.php +++ b/app/code/Magento/Newsletter/Controller/Manage/Save.php @@ -77,7 +77,7 @@ public function execute() $customerId = $this->_customerSession->getCustomerId(); if ($customerId === null) { - $this->messageManager->addError(__('Something went wrong while saving your subscription.')); + $this->messageManager->addErrorMessage(__('Something went wrong while saving your subscription.')); } else { try { $customer = $this->customerRepository->getById($customerId); @@ -105,7 +105,7 @@ public function execute() $this->messageManager->addSuccess(__('We have updated your subscription.')); } } catch (\Exception $e) { - $this->messageManager->addError(__('Something went wrong while saving your subscription.')); + $this->messageManager->addErrorMessage(__('Something went wrong while saving your subscription.')); } } return $this->_redirect('customer/account/'); diff --git a/app/code/Magento/Newsletter/Model/SubscriptionManager.php b/app/code/Magento/Newsletter/Model/SubscriptionManager.php index 846d095625e0c..57c6cd8b843a7 100644 --- a/app/code/Magento/Newsletter/Model/SubscriptionManager.php +++ b/app/code/Magento/Newsletter/Model/SubscriptionManager.php @@ -195,12 +195,14 @@ private function saveSubscriber( ): bool { $statusChanged = (int)$subscriber->getStatus() !== $status; $emailChanged = $subscriber->getEmail() !== $customer->getEmail(); - if ($subscriber->getId() - && !$statusChanged - && (int)$subscriber->getCustomerId() === (int)$customer->getId() - && (int)$subscriber->getStoreId() === $storeId - && !$emailChanged - ) { + if ($this->dontNeedToSaveSubscriber( + $subscriber, + $customer, + $statusChanged, + $storeId, + $status, + $emailChanged + )) { return false; } @@ -220,10 +222,37 @@ private function saveSubscriber( /** * If the subscriber is waiting to confirm from the customer - * and customer changed the email + * or customer changed the email * than need to send confirmation letter to the new email */ - return $status === Subscriber::STATUS_NOT_ACTIVE && $emailChanged; + return $status === Subscriber::STATUS_NOT_ACTIVE || $emailChanged; + } + + /** + * Don't need to save subscriber model + * + * @param Subscriber $subscriber + * @param CustomerInterface $customer + * @param bool $statusChanged + * @param int $storeId + * @param int $status + * @param bool $emailChanged + * @return bool + */ + private function dontNeedToSaveSubscriber( + Subscriber $subscriber, + CustomerInterface $customer, + bool $statusChanged, + int $storeId, + int $status, + bool $emailChanged + ): bool { + return $subscriber->getId() + && !$statusChanged + && (int)$subscriber->getCustomerId() === (int)$customer->getId() + && (int)$subscriber->getStoreId() === $storeId + && !$emailChanged + && $status !== Subscriber::STATUS_NOT_ACTIVE; } /** diff --git a/app/code/Magento/Newsletter/Test/Unit/Controller/Manage/SaveTest.php b/app/code/Magento/Newsletter/Test/Unit/Controller/Manage/SaveTest.php index f333467732e30..e5b09c2e89852 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Controller/Manage/SaveTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Controller/Manage/SaveTest.php @@ -122,7 +122,7 @@ public function testSaveActionInvalidFormKey() $this->messageManagerMock->expects($this->never()) ->method('addSuccess'); $this->messageManagerMock->expects($this->never()) - ->method('addError'); + ->method('addErrorMessage'); $this->action->execute(); } @@ -140,7 +140,7 @@ public function testSaveActionNoCustomerInSession() $this->messageManagerMock->expects($this->never()) ->method('addSuccess'); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('Something went wrong while saving your subscription.'); $this->action->execute(); } @@ -169,7 +169,7 @@ public function testSaveActionWithException() $this->messageManagerMock->expects($this->never()) ->method('addSuccess'); $this->messageManagerMock->expects($this->once()) - ->method('addError') + ->method('addErrorMessage') ->with('Something went wrong while saving your subscription.'); $this->action->execute(); } diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php index 4e1f18a26a95a..6139d86191f44 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php @@ -454,7 +454,7 @@ public function subscribeCustomerDataProvider(): array 'subscriber_status' => Subscriber::STATUS_SUBSCRIBED, 'subscriber_confirm_code' => '', ], - 'needToSendEmail' => false, + 'needToSendEmail' => true, ], 'Update subscription data: subscription confirm required ' => [ 'subscriber_data' => [ @@ -618,7 +618,7 @@ public function unsubscribeCustomerDataProvider(): array 'subscriber_status' => Subscriber::STATUS_NOT_ACTIVE, 'subscriber_confirm_code' => '', ], - 'needToSendEmail' => false, + 'needToSendEmail' => true, ], 'Update subscription data' => [ 'subscriber_data' => [ @@ -642,7 +642,7 @@ public function unsubscribeCustomerDataProvider(): array 'subscriber_status' => Subscriber::STATUS_UNSUBSCRIBED, 'subscriber_confirm_code' => '', ], - 'needToSendEmail' => false, + 'needToSendEmail' => true, ], ]; } diff --git a/app/code/Magento/PageCache/Plugin/AppendNoStoreCacheHeader.php b/app/code/Magento/PageCache/Plugin/AppendNoStoreCacheHeader.php new file mode 100644 index 0000000000000..fc18855a51710 --- /dev/null +++ b/app/code/Magento/PageCache/Plugin/AppendNoStoreCacheHeader.php @@ -0,0 +1,30 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Plugin; + +use Magento\Framework\App\FrontControllerInterface; +use Magento\Framework\App\Response\HttpInterface; + +class AppendNoStoreCacheHeader +{ + /** + * Set cache-control header + * + * @param FrontControllerInterface $controller + * @param HttpInterface $response + * @return HttpInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDispatch(FrontControllerInterface $controller, HttpInterface $response): HttpInterface + { + $response->setHeader('Cache-Control', 'no-store'); + return $response; + } +} diff --git a/app/code/Magento/PageCache/etc/webapi_rest/di.xml b/app/code/Magento/PageCache/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..04906a615a9df --- /dev/null +++ b/app/code/Magento/PageCache/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\Framework\App\FrontControllerInterface"> + <plugin name="append_no_store_cache_header" type="Magento\PageCache\Plugin\AppendNoStoreCacheHeader" /> + </type> +</config> diff --git a/app/code/Magento/PageCache/etc/webapi_soap/di.xml b/app/code/Magento/PageCache/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..04906a615a9df --- /dev/null +++ b/app/code/Magento/PageCache/etc/webapi_soap/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\Framework\App\FrontControllerInterface"> + <plugin name="append_no_store_cache_header" type="Magento\PageCache\Plugin\AppendNoStoreCacheHeader" /> + </type> +</config> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml index 898c26bb4b45a..cea228ac7a344 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -16,6 +16,9 @@ <description value="Users are able to place order using Paypal Smart Button on Checkout Page, payment action is Sale"/> <severity value="CRITICAL"/> <testCaseId value="MC-13690"/> + <skip> + <issueId value="MC-37236"/> + </skip> <group value="paypalExpress"/> </annotations> <before> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml index 3fd5f44d5a4b6..a4d99ecbf7e61 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml @@ -16,6 +16,9 @@ <description value="Users are able to place order using Paypal Smart Button using Euro currency and merchant country is France"/> <severity value="MAJOR"/> <testCaseId value="MC-33274"/> + <skip> + <issueId value="MC-37236"/> + </skip> <group value="paypalExpress"/> </annotations> <before> diff --git a/app/code/Magento/Persistent/Model/Customer/Authorization.php b/app/code/Magento/Persistent/Model/Customer/Authorization.php new file mode 100644 index 0000000000000..6d8859a30fd96 --- /dev/null +++ b/app/code/Magento/Persistent/Model/Customer/Authorization.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model\Customer; + +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\AuthorizationInterface; +use Magento\Persistent\Helper\Session as PersistentSession; + +/** + * Authorization logic for persistent customers + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class Authorization implements AuthorizationInterface +{ + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var PersistentSession + */ + private $persistentSession; + + /** + * @param CustomerSession $customerSession + * @param PersistentSession $persistentSession + */ + public function __construct( + CustomerSession $customerSession, + PersistentSession $persistentSession + ) { + $this->customerSession = $customerSession; + $this->persistentSession = $persistentSession; + } + + /** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function isAllowed( + $resource, + $privilege = null + ) { + if ($this->persistentSession->isPersistent() && !$this->customerSession->isLoggedIn()) { + return false; + } + + return true; + } +} diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml index 43390598f7cb3..4f68c055f2615 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/GuestCheckoutWithEnabledPersistentTest.xml @@ -48,8 +48,7 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingGuestInfoSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <!-- Check that have the same values after page reload --> - <amOnPage url="{{CheckoutPage.url}}" stepKey="amOnCheckoutShippingInfoPage"/> - <waitForPageLoad stepKey="waitForShippingPageReload"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="amOnCheckoutShippingInfoPage"/> <seeInField selector="{{CheckoutShippingGuestInfoSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="seeEmailOnCheckout" /> <seeInField selector="{{CheckoutShippingGuestInfoSection.firstName}}" userInput="{{CustomerEntityOne.firstName}}" stepKey="seeFirstnameOnCheckout" /> <seeInField selector="{{CheckoutShippingGuestInfoSection.lastName}}" userInput="{{CustomerEntityOne.lastName}}" stepKey="seeLastnameOnCheckout" /> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml index b41cad61c93a5..45ccab54de5f3 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontCorrectWelcomeMessageAfterCustomerIsLoggedOutTest.xml @@ -54,7 +54,7 @@ stepKey="seeLoggedInCustomerWelcomeMessage"/> <!--Logout and check default welcome message--> <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="storefrontCustomerLogout"/> - <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" stepKey="seeCustomerSignOutPageUrl"/> + <actionGroup ref="AssertStorefrontCustomerLogoutSuccessPageActionGroup" stepKey="seeCustomerSignOutPageUrl"/> <see userInput="Default welcome msg!" selector="{{StorefrontHeaderSection.welcomeMessage}}" stepKey="seeDefaultWelcomeMessage"/> @@ -71,7 +71,7 @@ <!--Logout and check persistent customer welcome message--> <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="storefrontCustomerLogout1"/> - <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" stepKey="seeCustomerSignOutPageUrl1"/> + <actionGroup ref="AssertStorefrontCustomerLogoutSuccessPageActionGroup" stepKey="seeCustomerSignOutPageUrl1"/> <see userInput="Welcome, $$createCustomerForPersistent.firstname$$ $$createCustomerForPersistent.lastname$$! Not you?" selector="{{StorefrontHeaderSection.welcomeMessage}}" stepKey="seePersistentWelcomeMessage"/> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml index 80ca7a2eb90c7..b3bf4e4dd009f 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml @@ -73,7 +73,7 @@ <!-- 4. Click Sign Out --> <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="logoutJohnSmithCustomer"/> - <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" stepKey="seeLogoutSuccessPageUrlAfterLogOutJohnSmithCustomer"/> + <actionGroup ref="AssertStorefrontCustomerLogoutSuccessPageActionGroup" stepKey="seeLogoutSuccessPageUrlAfterLogOutJohnSmithCustomer"/> <waitForPageLoad stepKey="waitForRedirectToHomePage"/> <waitForText selector="{{StorefrontCMSPageSection.mainContent}}" userInput="CMS homepage content goes here." stepKey="waitForLoadContentMessage"/> <actionGroup ref="StorefrontAssertPersistentCustomerWelcomeMessageNotPresentActionGroup" stepKey="dontSeeWelcomeJohnSmithCustomerNotYouMessage"> @@ -102,7 +102,7 @@ <!-- 7. Click Log Out --> <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="logoutJohnDoeCustomer"/> - <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" stepKey="seeLogoutSuccessPageUrlAfterLogOutJohnDoeCustomer"/> + <actionGroup ref="AssertStorefrontCustomerLogoutSuccessPageActionGroup" stepKey="seeLogoutSuccessPageUrlAfterLogOutJohnDoeCustomer"/> <actionGroup ref="StorefrontAssertPersistentCustomerWelcomeMessageActionGroup" stepKey="seeWelcomeForJohnDoeCustomer"> <argument name="customerFullName" value="{{Simple_Customer_Without_Address.fullname}}"/> </actionGroup> @@ -149,7 +149,7 @@ <!-- 12. Sign out and click the Not you? link --> <actionGroup ref="CustomerLogoutStorefrontByMenuItemsActionGroup" stepKey="logoutJohnDoeCustomerSecondTime"/> - <seeInCurrentUrl url="{{StorefrontCustomerLogoutSuccessPage.url}}" stepKey="seeLogoutSuccessPageUrlAfterLogOutJohnSmithCustomerSecondTime"/> + <actionGroup ref="AssertStorefrontCustomerLogoutSuccessPageActionGroup" stepKey="seeLogoutSuccessPageUrlAfterLogOutJohnSmithCustomerSecondTime"/> <waitForPageLoad stepKey="waitForHomePageLoadAfter5Seconds"/> <waitForText selector="{{StorefrontCMSPageSection.mainContent}}" userInput="CMS homepage content goes here." stepKey="waitForLoadMainContentMessageOnHomePage"/> <click selector="{{StorefrontPanelHeaderSection.notYouLink}}" stepKey="clickOnNotYouLink" /> diff --git a/app/code/Magento/Persistent/Test/Unit/Model/Customer/AuthorizationTest.php b/app/code/Magento/Persistent/Test/Unit/Model/Customer/AuthorizationTest.php new file mode 100644 index 0000000000000..d2abafc7e5ecf --- /dev/null +++ b/app/code/Magento/Persistent/Test/Unit/Model/Customer/AuthorizationTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Test\Unit\Model\Customer; + +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Persistent\Helper\Session as PersistentSession; +use Magento\Persistent\Model\Customer\Authorization as PersistentAuthorization; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Magento\Customer\Model\Customer\AuthorizationComposite as CustomerAuthorizationComposite; + +/** + * A test class for the persistent customers authorization + * + * Unit tests for \Magento\Persistent\Model\Customer\Authorization class. + */ +class AuthorizationTest extends TestCase +{ + /** + * @var PersistentSession|MockObject + */ + private $persistentSessionMock; + + /** + * @var PersistentAuthorization + */ + private $persistentCustomerAuthorization; + + /** + * @var CustomerSession|MockObject + */ + private $customerSessionMock; + + /** + * @var CustomerAuthorizationComposite + */ + private $customerAuthorizationComposite; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->persistentSessionMock = $this->getMockBuilder(PersistentSession::class) + ->onlyMethods(['isPersistent']) + ->disableOriginalConstructor() + ->getMock(); + + $this->customerSessionMock = $this->getMockBuilder(CustomerSession::class) + ->onlyMethods(['isLoggedIn']) + ->disableOriginalConstructor() + ->getMock(); + + $this->persistentCustomerAuthorization = new PersistentAuthorization( + $this->customerSessionMock, + $this->persistentSessionMock + ); + + $this->customerAuthorizationComposite = new CustomerAuthorizationComposite( + [$this->persistentCustomerAuthorization] + ); + } + + /** + * Validate if isAuthorized() will return proper permission value for logged in/ out persistent customers + * + * @dataProvider persistentLoggedInCombinations + * @param bool $isPersistent + * @param bool $isLoggedIn + * @param bool $isAllowedExpectation + */ + public function testIsAuthorized( + bool $isPersistent, + bool $isLoggedIn, + bool $isAllowedExpectation + ): void { + $this->persistentSessionMock->method('isPersistent')->willReturn($isPersistent); + $this->customerSessionMock->method('isLoggedIn')->willReturn($isLoggedIn); + $isAllowedResult = $this->customerAuthorizationComposite->isAllowed('self'); + + $this->assertEquals($isAllowedExpectation, $isAllowedResult); + } + + /** + * @return array + */ + public function persistentLoggedInCombinations(): array + { + return [ + [ + true, + false, + false + ], + [ + true, + true, + true + ], + [ + false, + false, + true + ], + ]; + } +} diff --git a/app/code/Magento/Persistent/etc/di.xml b/app/code/Magento/Persistent/etc/di.xml index f49d4361acb52..fd1c97fae66d9 100644 --- a/app/code/Magento/Persistent/etc/di.xml +++ b/app/code/Magento/Persistent/etc/di.xml @@ -12,4 +12,14 @@ <type name="Magento\Customer\CustomerData\Customer"> <plugin name="section_data" type="Magento\Persistent\Model\Plugin\CustomerData" /> </type> + <type name="Magento\Persistent\Model\Customer\Authorization"> + <arguments> + <argument name="customerSession" xsi:type="object">Magento\Customer\Model\Session\Proxy</argument> + </arguments> + </type> + <type name="Magento\Persistent\Helper\Session"> + <arguments> + <argument name="checkoutSession" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/webapi_rest/di.xml b/app/code/Magento/Persistent/etc/webapi_rest/di.xml index cb0aec6b460af..89504f0471788 100644 --- a/app/code/Magento/Persistent/etc/webapi_rest/di.xml +++ b/app/code/Magento/Persistent/etc/webapi_rest/di.xml @@ -13,4 +13,11 @@ <plugin name="persistent_convert_customer_cart_to_guest_cart" type="Magento\Persistent\Model\Checkout\GuestShippingInformationManagementPlugin"/> </type> + <type name="Magento\Customer\Model\Customer\AuthorizationComposite"> + <arguments> + <argument name="authorizationChecks" xsi:type="array"> + <item name="persistent_rest_customer_authorization" xsi:type="object">Magento\Persistent\Model\Customer\Authorization</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Persistent/etc/webapi_soap/di.xml b/app/code/Magento/Persistent/etc/webapi_soap/di.xml index cb0aec6b460af..2a440fff03598 100644 --- a/app/code/Magento/Persistent/etc/webapi_soap/di.xml +++ b/app/code/Magento/Persistent/etc/webapi_soap/di.xml @@ -13,4 +13,11 @@ <plugin name="persistent_convert_customer_cart_to_guest_cart" type="Magento\Persistent\Model\Checkout\GuestShippingInformationManagementPlugin"/> </type> + <type name="Magento\Customer\Model\Customer\AuthorizationComposite"> + <arguments> + <argument name="authorizationChecks" xsi:type="array"> + <item name="persistent_soap_customer_authorization" xsi:type="object">Magento\Persistent\Model\Customer\Authorization</item> + </argument> + </arguments> + </type> </config> 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 a1228903e2323..c19dbc2c429ae 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -119,9 +119,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) $groupId = null; if (empty($customerVatNumber) || false == $this->customerVat->isCountryInEU($customerCountryCode)) { - $groupId = $customer->getId() ? $this->groupManagement->getDefaultGroup( - $storeId - )->getId() : $this->groupManagement->getNotLoggedInGroup()->getId(); + $groupId = $customer->getId() ? $quote->getCustomerGroupId() : + $this->groupManagement->getNotLoggedInGroup()->getId(); } else { // Magento always has to emulate group even if customer uses default billing/shipping address $groupId = $this->customerVat->getCustomerGroupIdBasedOnVatNumber( 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 1920b088b1c0e..ae2a4734215ad 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 @@ -13,7 +13,7 @@ use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Customer\Api\GroupManagementInterface; -use Magento\Customer\Helper\Address; +use Magento\Customer\Helper\Address as CustomerAddress; use Magento\Customer\Model\Session; use Magento\Customer\Model\Vat; use Magento\Framework\Event\Observer; @@ -21,10 +21,11 @@ use Magento\Quote\Api\Data\ShippingAssignmentInterface; use Magento\Quote\Api\Data\ShippingInterface; use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address; use Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver; use Magento\Quote\Observer\Frontend\Quote\Address\VatValidator; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; /** * Class CollectTotalsTest @@ -124,7 +125,7 @@ protected function setUp(): void true, ['getStoreId', 'getCustomAttribute', 'getId', '__wakeup'] ); - $this->customerAddressMock = $this->createMock(Address::class); + $this->customerAddressMock = $this->createMock(CustomerAddress::class); $this->customerVatMock = $this->createMock(Vat::class); $this->customerDataFactoryMock = $this->getMockBuilder(CustomerInterfaceFactory::class) ->addMethods(['mergeDataObjectWithArray']) @@ -174,6 +175,7 @@ protected function setUp(): void $shippingAssignmentMock = $this->getMockForAbstractClass(ShippingAssignmentInterface::class); $shippingMock = $this->getMockForAbstractClass(ShippingInterface::class); + $shippingAssignmentMock->expects($this->once())->method('getShipping')->willReturn($shippingMock); $shippingMock->expects($this->once())->method('getAddress')->willReturn($this->quoteAddressMock); @@ -185,7 +187,6 @@ protected function setUp(): void $this->quoteMock->expects($this->any()) ->method('getCustomer') ->willReturn($this->customerMock); - $this->addressRepository = $this->getMockForAbstractClass(AddressRepositoryInterface::class); $this->customerSession = $this->getMockBuilder(Session::class) ->disableOriginalConstructor() @@ -266,26 +267,20 @@ public function testDispatchWithDefaultCustomerGroupId() ->willReturn('customerCountryCode'); $this->quoteAddressMock->expects($this->once())->method('getVatId')->willReturn(null); - $this->quoteMock->expects($this->once()) + $this->quoteMock->expects($this->exactly(2)) ->method('getCustomerGroupId') ->willReturn('customerGroupId'); $this->customerMock->expects($this->once())->method('getId')->willReturn('1'); - $this->groupManagementMock->expects($this->once()) - ->method('getDefaultGroup') - ->willReturn($this->groupInterfaceMock); - $this->groupInterfaceMock->expects($this->once()) - ->method('getId')->willReturn('defaultCustomerGroupId'); + /** Assertions */ $this->quoteAddressMock->expects($this->once()) ->method('setPrevQuoteCustomerGroupId') ->with('customerGroupId'); - $this->quoteMock->expects($this->once())->method('setCustomerGroupId')->with('defaultCustomerGroupId'); $this->customerDataFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->customerMock); $this->quoteMock->expects($this->once())->method('setCustomer')->with($this->customerMock); - /** SUT execution */ $this->model->execute($this->observerMock); } @@ -343,7 +338,7 @@ public function testDispatchWithAddressCustomerVatIdAndCountryId() $customerVat = "123123123"; $defaultShipping = 1; - $customerAddress = $this->createMock(\Magento\Quote\Model\Quote\Address::class); + $customerAddress = $this->createMock(Address::class); $customerAddress->expects($this->any()) ->method("getVatId") ->willReturn($customerVat); @@ -379,8 +374,8 @@ public function testDispatchWithEmptyShippingAddress() $customerCountryCode = "DE"; $customerVat = "123123123"; $defaultShipping = 1; - $customerAddress = $this->getMockForAbstractClass(AddressInterface::class); + $customerAddress->expects($this->once()) ->method("getCountryId") ->willReturn($customerCountryCode); diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php index 7ad2e5dde2985..e14d8bde6be74 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -110,6 +110,10 @@ private function findRelations(array $products, array $loadAttributes, int $link //Matching products with related products. $relationsData = []; foreach ($relations as $productId => $relatedIds) { + //Remove related products that not exist in map list. + $relatedIds = array_filter($relatedIds, function ($relatedId) use ($relatedProducts) { + return isset($relatedProducts[$relatedId]); + }); $relationsData[$productId] = array_map( function ($id) use ($relatedProducts) { return $relatedProducts[$id]; diff --git a/app/code/Magento/Review/Block/Adminhtml/Add.php b/app/code/Magento/Review/Block/Adminhtml/Add.php index 2edd76879d8dc..5f739b2595418 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add.php @@ -27,15 +27,10 @@ protected function _construct() $this->_mode = 'add'; $this->buttonList->update('save', 'label', __('Save Review')); $this->buttonList->update('save', 'id', 'save_button'); + $this->buttonList->update('save', 'style', 'display: none;'); $this->buttonList->update('reset', 'id', 'reset_button'); + $this->buttonList->update('reset', 'style', 'display: none;'); $this->buttonList->update('reset', 'onclick', 'window.review.formReset()'); - $this->_formScripts[] = ' - require(["prototype"], function(){ - toggleParentVis("add_review_form"); - toggleVis("save_button"); - toggleVis("reset_button"); - }); - '; // @codingStandardsIgnoreStart $this->_formInitScripts[] = ' require(["jquery","Magento_Review/js/rating","prototype"], function(jQuery, rating){ diff --git a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php index 04e6343eb43ca..efffa7a02678a 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Add/Form.php +++ b/app/code/Magento/Review/Block/Adminhtml/Add/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Review\Block\Adminhtml\Add; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml add product review form * @@ -26,6 +29,11 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic */ protected $_systemStore; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -33,6 +41,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Store\Model\System\Store $systemStore * @param \Magento\Review\Helper\Data $reviewData * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -40,10 +49,12 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Store\Model\System\Store $systemStore, \Magento\Review\Helper\Data $reviewData, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null ) { $this->_reviewData = $reviewData; $this->_systemStore = $systemStore; + $this->secureRenderer = $htmlRenderer ?: ObjectManager::getInstance()->get(SecureHtmlRenderer::class); parent::__construct($context, $registry, $formFactory, $data); } @@ -59,6 +70,8 @@ protected function _prepareForm() $form = $this->_formFactory->create(); $fieldset = $form->addFieldset('add_review_form', ['legend' => __('Review Details')]); + $beforeHtml = $this->secureRenderer->renderStyleAsTag('display: none;', '#edit_form'); + $fieldset->setBeforeElementHtml($beforeHtml); $fieldset->addField('product_name', 'note', ['label' => __('Product'), 'text' => 'product_name']); diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index d5a94a4dd1fcf..8ef12e5889520 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -745,10 +745,12 @@ public function getCustomerCart() try { $this->_cart = $this->quoteRepository->getForCustomer($customerId, [$storeId]); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - $this->_cart->setStore($this->getSession()->getStore()); - $customerData = $this->customerRepository->getById($customerId); - $this->_cart->assignCustomer($customerData); - $this->quoteRepository->save($this->_cart); + if ($this->getQuote()->hasItems()) { + $this->_cart->setStore($this->getSession()->getStore()); + $customerData = $this->customerRepository->getById($customerId); + $this->_cart->assignCustomer($customerData); + $this->quoteRepository->save($this->_cart); + } } } @@ -785,6 +787,7 @@ public function getCustomerCompareList() public function getCustomerGroupId() { $groupId = $this->getQuote()->getCustomerGroupId(); + // @phpstan-ignore-next-line if (!isset($groupId)) { $groupId = $this->getSession()->getCustomerGroupId(); } @@ -1443,9 +1446,10 @@ public function setShippingAddress($address) */ $saveInAddressBook = (int)(!empty($address['save_in_address_book'])); $shippingAddress->setData('save_in_address_book', $saveInAddressBook); - } - if ($address instanceof \Magento\Quote\Model\Quote\Address) { + } elseif ($address instanceof \Magento\Quote\Model\Quote\Address) { $shippingAddress = $address; + } else { + $shippingAddress = null; } $this->setRecollect(true); diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order.php b/app/code/Magento/Sales/Model/ResourceModel/Order.php index fd69f3b1a52a3..1903308466498 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order.php @@ -53,12 +53,12 @@ protected function _construct() /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context - * @param Attribute $attribute - * @param Manager $sequenceManager * @param Snapshot $entitySnapshot * @param RelationComposite $entityRelationComposite + * @param Attribute $attribute + * @param Manager $sequenceManager * @param StateHandler $stateHandler - * @param string $connectionName + * @param string|null $connectionName */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -137,6 +137,8 @@ protected function calculateItems(\Magento\Sales\Model\Order $object) } /** + * Before save + * * @param \Magento\Framework\Model\AbstractModel $object * @return $this */ @@ -152,15 +154,15 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) ]; $object->setStoreName(implode(PHP_EOL, $name)); $object->setTotalItemCount($this->calculateItems($object)); + $object->setData( + 'protect_code', + substr( + hash('sha256', uniqid(Random::getRandomNumber(), true) . ':' . microtime(true)), + 5, + 32 + ) + ); } - $object->setData( - 'protect_code', - substr( - hash('sha256', uniqid(Random::getRandomNumber(), true) . ':' . microtime(true)), - 5, - 32 - ) - ); $isNewCustomer = !$object->getCustomerId() || $object->getCustomerId() === true; if ($isNewCustomer && $object->getCustomer()) { $object->setCustomerId($object->getCustomer()->getId()); @@ -169,7 +171,7 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) } /** - * {@inheritdoc} + * @inheritdoc */ public function save(\Magento\Framework\Model\AbstractModel $object) { diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml index 92c01cf380746..4d75589c40e9c 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminInvoiceItemsSection.xml @@ -28,5 +28,6 @@ <element name="discountAmountColumn" type="text" selector=".order-invoice-tables .col-discount .price"/> <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']"/> + <element name="bundleItem" type="text" selector="#invoice_item_container .option-value"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml index 6f4073bf70f46..127fd1dd4e006 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddConfigurableProductToOrderFromShoppingCartTest.xml @@ -97,8 +97,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCreatedCustomer"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEditButton"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickEditButton"/> <!-- Click create order --> <click selector="{{AdminCustomerMainActionsSection.createOrderBtn}}" stepKey="clickCreateOrder"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml index d8a9effa56dac..701b7ebe4a958 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AddSimpleProductToOrderFromShoppingCartTest.xml @@ -58,8 +58,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCreatedCustomer"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEditButton"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickEditButton"/> <!-- Click create order --> <click selector="{{AdminCustomerMainActionsSection.createOrderBtn}}" stepKey="clickCreateOrder"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml index 627f739852ee7..30f8386b3bb91 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml @@ -34,7 +34,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="addToCart"/> <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickCart"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index 7444de8271ed9..e41e3acbae380 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -64,7 +64,7 @@ <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onCategoryPage"/> <waitForPageLoad stepKey="waitForPageLoad1"/> <moveMouseOver selector="{{StorefrontCategoryMainSection.ProductItemInfo}}" stepKey="hoverOverProduct"/> - <click selector="{{StorefrontCategoryMainSection.AddToCartBtn}}" stepKey="addToCart"/> + <actionGroup ref="StorefrontClickAddToCartButtonActionGroup" stepKey="addToCart"/> <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductToAdd"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickCart"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml index c635e6b0ad6b2..8e9e117d2d995 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedConfigurableProductOnOrderPageTest.xml @@ -96,8 +96,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCreatedCustomer"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEditButton"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickEditButton"/> <!-- Click create order --> <click selector="{{AdminCustomerMainActionsSection.createOrderBtn}}" stepKey="clickCreateOrder"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml index eb28ebfd068da..71da699e533bc 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveLastOrderedSimpleProductOnOrderPageTest.xml @@ -46,8 +46,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCreatedCustomer"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEditButton"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickEditButton"/> <!-- Click create order --> <click selector="{{AdminCustomerMainActionsSection.createOrderBtn}}" stepKey="clickCreateOrder"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml index c3fc7a4952143..452d65ea5ae57 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml @@ -96,8 +96,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCreatedCustomer"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEditButton"/> - <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickEditButton"/> <!-- Click create order --> <click selector="{{AdminCustomerMainActionsSection.createOrderBtn}}" stepKey="clickCreateOrder"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml index 0e021600ab3e3..4d1ebddc7c2b3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedConfigurableProductOnOrderPageTest.xml @@ -99,8 +99,7 @@ <actionGroup ref="AdminFilterCustomerByEmail" stepKey="filterCreatedCustomer"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickEditButton"/> - <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickEditButton"/> <!-- Click create order --> <click selector="{{AdminCustomerMainActionsSection.createOrderBtn}}" stepKey="clickCreateOrder"/> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml index b2e84691a45cf..029bcb8abcc25 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/creditmemo/items/renderer/default.phtml @@ -10,15 +10,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtmlAttr(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,7 +27,7 @@ </div> <?php endif; ?> </dd> - <?php else : ?> + <?php else: ?> <dd> <?= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?> </dd> @@ -37,10 +37,10 @@ <?php endif; ?> <?php /* downloadable */ ?> - <?php if ($links = $block->getLinks()) : ?> + <?php if ($links = $block->getLinks()): ?> <dl class="item options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?></dt> - <?php foreach ($links->getPurchasedItems() as $link) : ?> + <?php foreach ($links->getPurchasedItems() as $link): ?> <dd><?= $block->escapeHtml($link->getLinkTitle()) ?></dd> <?php endforeach; ?> </dl> @@ -48,12 +48,14 @@ <?php /* EOF downloadable */ ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) : ?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> @@ -61,7 +63,9 @@ <td class="col subtotal" data-th="<?= $block->escapeHtml(__('Subtotal')) ?>"> <?= $block->getItemRowTotalHtml() ?> </td> - <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"><?= /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?></td> + <td class="col discount" data-th="<?= $block->escapeHtml(__('Discount Amount')) ?>"> + <?= /* @noEscape */ $_order->formatPrice(-$_item->getDiscountAmount()) ?> + </td> <td class="col total" data-th="<?= $block->escapeHtml(__('Row Total')) ?>"> <?= $block->getItemRowTotalAfterDiscountHtml() ?> </td> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml index 0176582f0fcd7..d9542d13aba6d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/invoice/items/renderer/default.phtml @@ -10,15 +10,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,19 +27,21 @@ </div> <?php endif; ?> </dd> - <?php else : ?> + <?php else: ?> <dd><?= $block->escapeHtml($_option['print_value'] ?? $_option['value']) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) :?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml index 51e43476238be..9cae232ca6541 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/items/renderer/default.phtml @@ -10,15 +10,15 @@ $_item = $block->getItem(); <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item-options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -27,43 +27,46 @@ $_item = $block->getItem(); </div> <?php endif; ?> </dd> - <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <?php else: ?> + <?php $optionValue = isset($_option['print_value']) ? $_option['print_value'] : $_option['value'] ?> + <dd><?= $block->escapeHtml($optionValue) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addtInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addtInfoBlock) : ?> + <?php if ($addtInfoBlock): ?> <?= $addtInfoBlock->setItem($_item)->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col price" data-th="<?= $block->escapeHtml(__('Price')) ?>"> <?= $block->getItemPriceHtml() ?> </td> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty')) ?>"> <ul class="items-qty"> - <?php if ($block->getItem()->getQtyOrdered() > 0) : ?> + <?php if ($block->getItem()->getQtyOrdered() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Ordered')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyOrdered() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyShipped() > 0) : ?> + <?php if ($block->getItem()->getQtyShipped() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Shipped')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyShipped() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyCanceled() > 0) : ?> + <?php if ($block->getItem()->getQtyCanceled() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Canceled')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyCanceled() ?></span> </li> <?php endif; ?> - <?php if ($block->getItem()->getQtyRefunded() > 0) : ?> + <?php if ($block->getItem()->getQtyRefunded() > 0): ?> <li class="item"> <span class="title"><?= $block->escapeHtml(__('Refunded')) ?></span> <span class="content"><?= (float) $block->getItem()->getQtyRefunded() ?></span> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml index 26fe74b0fc454..6c7567a8cd14b 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/shipment/items/renderer/default.phtml @@ -9,15 +9,15 @@ <tr id="order-item-row-<?= (int) $_item->getId() ?>"> <td class="col name" data-th="<?= $block->escapeHtml(__('Product Name')) ?>"> <strong class="product name product-item-name"><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_options = $block->getItemOptions()) : ?> + <?php if ($_options = $block->getItemOptions()): ?> <dl class="item options"> - <?php foreach ($_options as $_option) : ?> + <?php foreach ($_options as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> - <?php if (!$block->getPrintStatus()) : ?> + <?php if (!$block->getPrintStatus()): ?> <?php $_formatedOptionValue = $block->getFormatedOptionValue($_option) ?> <dd<?= (isset($_formatedOptionValue['full_view']) ? ' class="tooltip wrapper"' : '') ?>> - <?= $block->escapeHtml($_formatedOptionValue['value'], ['a', 'img']) ?> - <?php if (isset($_formatedOptionValue['full_view'])) : ?> + <?= $block->escapeHtml($_formatedOptionValue['value'], ['a']) ?> + <?php if (isset($_formatedOptionValue['full_view'])): ?> <div class="tooltip content"> <dl class="item options"> <dt><?= $block->escapeHtml($_option['label']) ?></dt> @@ -26,18 +26,21 @@ </div> <?php endif; ?> </dd> - <?php else : ?> - <dd><?= $block->escapeHtml((isset($_option['print_value']) ? $_option['print_value'] : $_option['value'])) ?></dd> + <?php else: ?> + <?php $optionValue = isset($_option['print_value']) ? $_option['print_value'] : $_option['value'] ?> + <dd><?= $block->escapeHtml($optionValue) ?></dd> <?php endif; ?> <?php endforeach; ?> </dl> <?php endif; ?> <?php $addInfoBlock = $block->getProductAdditionalInformationBlock(); ?> - <?php if ($addInfoBlock) : ?> + <?php if ($addInfoBlock): ?> <?= $addInfoBlock->setItem($_item->getOrderItem())->toHtml() ?> <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> </td> - <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"><?= /* @noEscape */ $block->prepareSku($block->getSku()) ?></td> + <td class="col sku" data-th="<?= $block->escapeHtml(__('SKU')) ?>"> + <?= /* @noEscape */ $block->prepareSku($block->getSku()) ?> + </td> <td class="col qty" data-th="<?= $block->escapeHtml(__('Qty Shipped')) ?>"><?= (int) $_item->getQty() ?></td> </tr> diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php index 53459f2c3e52f..d1440a2b547a4 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsCsv.php @@ -15,13 +15,14 @@ use Magento\Framework\View\Result\Layout; use Magento\Framework\App\ResponseInterface; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Export Coupons to csv file * * Class \Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCouponsCsv */ -class ExportCouponsCsv extends Quote implements HttpGetActionInterface +class ExportCouponsCsv extends Quote implements HttpGetActionInterface, HttpPostActionInterface { /** * Export coupon codes as CSV file diff --git a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php index fa3d4455410c4..401d8aea1aded 100644 --- a/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php +++ b/app/code/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCouponsXml.php @@ -15,13 +15,14 @@ use Magento\Framework\View\Result\Layout; use Magento\Framework\App\ResponseInterface; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; /** * Export coupons to xml file * * Class \Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCouponsXml */ -class ExportCouponsXml extends Quote implements HttpGetActionInterface +class ExportCouponsXml extends Quote implements HttpGetActionInterface, HttpPostActionInterface { /** * Export coupon codes as excel xml file diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml index 90b591a7bb1b1..c8ad0efdd4b4d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/DiscountSection.xml @@ -14,6 +14,7 @@ <element name="ApplyCodeBtn" type="button" selector="//span[text()='Apply Discount']"/> <element name="CancelCoupon" type="button" selector="//button[@value='Cancel Coupon']"/> <element name="DiscountVerificationMsg" type="text" selector=".message-success div"/> + <element name="DiscountVerificationMsgWithAriaAtomicProperty" type="text" selector=".message-success[aria-atomic=true] div"/> <element name="CancelCouponBtn" type="button" selector="#discount-form .action-cancel"/> </section> </sections> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml index f32442ca5bc98..f956d036d7080 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml @@ -39,8 +39,7 @@ <!--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"/> + <actionGroup ref="AdminOpenGeneralConfigurationPageActionGroup" stepKey="goToGeneralConfig"/> <wait stepKey="wait" time="10"/> <conditionalClick selector="{{LocaleOptionsSection.sectionHeader}}" dependentSelector="{{LocaleOptionsSection.timezone}}" visible="false" stepKey="openLocaleSection"/> <grabValueFrom selector="{{LocaleOptionsSection.timezone}}" stepKey="originalTimezone"/> @@ -93,8 +92,7 @@ <see selector="{{CheckoutCartSummarySection.discountAmount}}" userInput="-$5.00" stepKey="seeDiscountTotal"/> <!--Reset timezone--> - <amOnPage url="{{GeneralConfigurationPage.url}}" stepKey="goToGeneralConfigReset"/> - <waitForPageLoad stepKey="waitForConfigPageReset"/> + <actionGroup ref="AdminOpenGeneralConfigurationPageActionGroup" stepKey="goToGeneralConfigReset"/> <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"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForBundleProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForBundleProductTest.xml new file mode 100644 index 0000000000000..101c72b78078a --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForBundleProductTest.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="CartPriceRuleForBundleProductTest"> + <annotations> + <features value="SalesRule"/> + <stories value="MAGETWO-28921 - Cart Price Rule for bundle products"/> + <title value="Checking Cart Price Rule for bundle products"/> + <description value="Checking Cart Price Rule for bundle products"/> + <severity value="BLOCKER"/> + <testCaseId value="MAGETWO-28921"/> + <group value="SalesRule"/> + </annotations> + + <before> + <!--Create 4 simple products--> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">5.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">3.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct3"> + <field key="price">7.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct4"> + <field key="price">18.00</field> + </createData> + + <!-- Create the bundle product based --> + <createData entity="ApiBundleProduct" stepKey="createBundleProduct" /> + <createData entity="CheckboxOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + </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 ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Make Attribute 'sku' accessible for Promo Rule Conditions --> + <actionGroup ref="NavigateToEditProductAttributeActionGroup" stepKey="editSkuAttribute"> + <argument name="ProductAttribute" value="sku" /> + </actionGroup> + <actionGroup ref="ChangeUseForPromoRuleConditionsProductAttributeActionGroup" stepKey="changeAttributePromoRule"> + <argument name="option" value="1" /> + </actionGroup> + + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices" /> + </before> + + <after> + <!-- Delete created SalesRule --> + <actionGroup ref="DeleteCartPriceRuleByName" stepKey="DeleteCartPriceRuleByName"> + <argument name="ruleName" value="{{SimpleSalesRule.name}}"/> + </actionGroup> + + <!-- Delete Bundle product and it's children --> + <deleteData createDataKey="createBundleProduct" stepKey="createBundleProduct" /> + <deleteData createDataKey="simpleProduct1" stepKey="simpleProduct1" /> + <deleteData createDataKey="simpleProduct2" stepKey="simpleProduct2" /> + <deleteData createDataKey="simpleProduct3" stepKey="simpleProduct3" /> + <deleteData createDataKey="simpleProduct4" stepKey="simpleProduct4" /> + + <!-- Revert Attribute 'sku' to it's default value (not accessible for Promo Rule Conditions) --> + <actionGroup ref="NavigateToEditProductAttributeActionGroup" stepKey="editSkuAttribute"> + <argument name="ProductAttribute" value="sku" /> + </actionGroup> + <actionGroup ref="ChangeUseForPromoRuleConditionsProductAttributeActionGroup" stepKey="changeAttributePromoRule"> + <argument name="option" value="0" /> + </actionGroup> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices2" /> + </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="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <click selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="clickToExpandActions"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="10" stepKey="fillDiscountAmount"/> + <scrollTo selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="ScrollToApplyRuleForConditions"/> + <click selector="{{AdminCartPriceRulesFormSection.conditions}}" stepKey="ApplyRuleForConditions"/> + <waitForPageLoad stepKey="waitForDropDownOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.childAttribute}}" userInput="SKU" stepKey="selectAttribute"/> + <waitForPageLoad stepKey="waitForOperatorOpened"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('is')}}" stepKey="clickToChooseCondition"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.operator}}" userInput="is one of" stepKey="selectOperator"/> + <waitForPageLoad stepKey="waitForOperatorOpened1"/> + <click selector="{{AdminCartPriceRulesFormSection.condition('...')}}" stepKey="clickToChooseOption"/> + <waitForPageLoad stepKey="waitForConditionOpened2"/> + <fillField selector="{{AdminCartPriceRulesFormSection.actionValue}}" userInput="$$simpleProduct1.sku$$" stepKey="fillSkuToFilters"/> + <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="$$createBundleProduct.sku$$.html" stepKey="goToProductPage1"/> + <waitForPageLoad stepKey="waitForProductPageLoad1"/> + + <!--Click "Customize and Add to Cart" button--> + <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> + + <!-- Select two products --> + <click stepKey="selectProduct1" selector="{{StorefrontBundledSection.productCheckbox('1','1')}}"/> + <click stepKey="selectProduct2" selector="{{StorefrontBundledSection.productCheckbox('2','1')}}"/> + + <!--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="12.00"/> + <argument name="shipping" value="5.00"/> + <argument name="shippingMethod" value="Flat Rate - Fixed"/> + <argument name="total" value="16.50"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AssertStorefrontVerifySearchButtonIsDisabledActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AssertStorefrontVerifySearchButtonIsDisabledActionGroup.xml new file mode 100644 index 0000000000000..57d39e35d539e --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AssertStorefrontVerifySearchButtonIsDisabledActionGroup.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"> + <!-- Filter by search query and select --> + <actionGroup name="AssertStorefrontVerifySearchButtonIsDisabledActionGroup"> + <annotations> + <description>Verify search button has disabled attribute</description> + </annotations> + + <grabAttributeFrom selector="{{StorefrontQuickSearchSection.searchButton}}" userInput="disabled" stepKey="grabSearchButtonDisabledAttribute"/> + + <assertEquals stepKey="assertSearchButtonDisabled"> + <actualResult type="const">$grabSearchButtonDisabledAttribute</actualResult> + <expectedResult type="string">true</expectedResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AssertStorefrontVerifySearchButtonIsEnabledActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AssertStorefrontVerifySearchButtonIsEnabledActionGroup.xml new file mode 100644 index 0000000000000..2e1f8d4b68d36 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AssertStorefrontVerifySearchButtonIsEnabledActionGroup.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"> + <!-- Filter by search query and select --> + <actionGroup name="AssertStorefrontVerifySearchButtonIsEnabledActionGroup"> + <annotations> + <description>Verify search button does not disabled attribute</description> + </annotations> + + <grabAttributeFrom selector="{{StorefrontQuickSearchSection.searchButton}}" userInput="disabled" stepKey="grabSearchButtonAttribute"/> + + <assertEmpty stepKey="assertSearchButtonEnabled"> + <actualResult type="string">$grabSearchButtonAttribute</actualResult> + </assertEmpty> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontFillSearchActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontFillSearchActionGroup.xml new file mode 100644 index 0000000000000..f90297df02c1f --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontFillSearchActionGroup.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="StoreFrontFillSearchActionGroup"> + <arguments> + <argument name="query" type="string"/> + </arguments> + + <fillField stepKey="fillSearchField" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="{{query}}"/> + <waitForElementVisible selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="waitForSubmitButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml index f5bb414f59197..88e459178edbc 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminMassDeleteSearchTermEntityTest.xml @@ -34,8 +34,7 @@ </after> <!-- Go to the catalog search term page --> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> - <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage"/> <!-- Select all created below search terms --> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByFirstSearchQuery"> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchButtonDisabledTillMinimumSearchLengthHitTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchButtonDisabledTillMinimumSearchLengthHitTest.xml new file mode 100644 index 0000000000000..742807d2c24e2 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchButtonDisabledTillMinimumSearchLengthHitTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySearchButtonDisabledTillMinimumSearchLengthHitTest"> + <annotations> + <stories value="Search Term Disabled"/> + <title value="Verify search button is disabled if search term is less than minimum search length"/> + <description value="Storefront verify search button is disabled if search term is less than minimum search length"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-37380"/> + <group value="searchFrontend"/> + </annotations> + + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> + + <actionGroup ref="StoreFrontFillSearchActionGroup" stepKey="fillSearchByTextLessThanMinimumSearchLength"> + <argument name="query" value="Te"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontVerifySearchButtonIsDisabledActionGroup" stepKey="assertSearchButtonIsDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchButtonEnabledAfterMinimumSearchLengthHitTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchButtonEnabledAfterMinimumSearchLengthHitTest.xml new file mode 100644 index 0000000000000..172fae919623c --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchButtonEnabledAfterMinimumSearchLengthHitTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontVerifySearchButtonEnabledAfterMinimumSearchLengthHitTest"> + <annotations> + <stories value="Search Button Not Disabled"/> + <title value="Verify search button is not disabled if search term is equal or greater than minimum search length"/> + <description value="Storefront verify search button is not disabled if search term is equal or greater than minimum search length"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-37381"/> + <group value="searchFrontend"/> + </annotations> + + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStoreFrontHomePage"/> + + <actionGroup ref="StoreFrontFillSearchActionGroup" stepKey="fillSearchByTextMoreThanMinimumSearchLength"> + <argument name="query" value="Magento"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontVerifySearchButtonIsEnabledActionGroup" stepKey="assertSearchButtonIsNotDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml index 22fcbfc2920ff..8c468cce91829 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml @@ -35,8 +35,7 @@ <!-- Delete created product --> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> <!-- Go to the catalog search term page --> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> - <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage"/> <!-- Filter the search term --> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByThirdSearchQuery"> <argument name="searchQuery" value="{{ApiProductDescription.value}}"/> @@ -60,8 +59,7 @@ <argument name="productName" value="$$simpleProduct.name$$"/> </actionGroup> <!-- Go to the catalog search term page --> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> - <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage"/> <!-- Filter the search term --> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByThirdSearchQuery"> <argument name="searchQuery" value="{{ApiProductDescription.value}}"/> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml index 0b02b49433dda..fb1f35730fd80 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml @@ -38,8 +38,7 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> <!-- Go to the catalog search term page --> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> - <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage"/> <!--Filter the search term --> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByThirdSearchQuery"> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml index d88bb023c60b2..1558f9aa5342b 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml @@ -39,8 +39,7 @@ <deleteData createDataKey="product" stepKey="deleteProduct"/> <!--Go to the catalog search term page --> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> - <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage"/> <!--Filter the search term --> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByThirdSearchQuery"> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml index 4c586d18fd3cf..19c12843c23a2 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml @@ -39,8 +39,7 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> <!-- Go to the catalog search term page --> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="openAdminCatalogSearchTermIndexPage"/> - <waitForPageLoad stepKey="waitForAdminCatalogSearchTermIndexPageLoad"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="openAdminCatalogSearchTermIndexPage"/> <!--Filter the search term --> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="filterByThirdSearchQuery"> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml index c5cbf1e0709c6..4f8cd9da856ca 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchTermEntityRedirectTest.xml @@ -32,7 +32,7 @@ </actionGroup> </before> <after> - <amOnPage url="{{AdminCatalogSearchTermIndexPage.url}}" stepKey="navigateToSearchTermPage"/> + <actionGroup ref="AdminOpenCatalogSearchTermIndexPageActionGroup" stepKey="navigateToSearchTermPage"/> <actionGroup ref="AdminSearchTermFilterBySearchQueryActionGroup" stepKey="findCreatedTerm"> <argument name="searchQuery" value="{{SearchTerm.query_text}}"/> </actionGroup> diff --git a/app/code/Magento/Search/view/frontend/web/js/form-mini.js b/app/code/Magento/Search/view/frontend/web/js/form-mini.js index b4493c5f38089..9b4c814f73d73 100644 --- a/app/code/Magento/Search/view/frontend/web/js/form-mini.js +++ b/app/code/Magento/Search/view/frontend/web/js/form-mini.js @@ -232,8 +232,10 @@ define([ break; case $.ui.keyCode.ENTER: - this.searchForm.trigger('submit'); - e.preventDefault(); + if (this.element.val().length >= parseInt(this.options.minSearchLength, 10)) { + this.searchForm.trigger('submit'); + e.preventDefault(); + } break; case $.ui.keyCode.DOWN: @@ -294,9 +296,10 @@ define([ dropdown = $('<ul role="listbox"></ul>'), value = this.element.val(); - this.submitBtn.disabled = isEmpty(value); + this.submitBtn.disabled = true; if (value.length >= parseInt(this.options.minSearchLength, 10)) { + this.submitBtn.disabled = false; $.getJSON(this.options.url, { q: value }, $.proxy(function (data) { diff --git a/app/code/Magento/Store/Block/Switcher.php b/app/code/Magento/Store/Block/Switcher.php index f15349f11066d..a924805fcba90 100644 --- a/app/code/Magento/Store/Block/Switcher.php +++ b/app/code/Magento/Store/Block/Switcher.php @@ -170,9 +170,15 @@ public function getGroups() if ($store) { $group->setHomeUrl($store->getHomeUrl()); + $group->setSortOrder($store->getSortOrder()); $groups[] = $group; } } + + usort($groups, static function ($itemA, $itemB) { + return (int)$itemA->getSortOrder() <=> (int)$itemB->getSortOrder(); + }); + $this->setData('groups', $groups); } return $this->getData('groups'); @@ -193,7 +199,12 @@ public function getStores() $stores = []; } else { $stores = $rawStores[$groupId]; + + uasort($stores, static function ($itemA, $itemB) { + return (int)$itemA->getSortOrder() <=> (int)$itemB->getSortOrder(); + }); } + $this->setData('stores', $stores); } return $this->getData('stores'); diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCheckStoreViewOptionsActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCheckStoreViewOptionsActionGroup.xml new file mode 100644 index 0000000000000..ba96633a621c2 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCheckStoreViewOptionsActionGroup.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="AdminCheckStoreViewOptionsActionGroup"> + <annotations> + <description>Goes to the Catalog->Product filters and check store view options at the Store View dropdown</description> + </annotations> + <arguments> + <argument name="storeViewId" type="string"/> + </arguments> + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <click selector="{{AdminProductFiltersSection.storeViewDropDown}}" stepKey="clickStoreViewSwitchDropdown"/> + <waitForElementVisible selector="{{AdminProductFiltersSection.storeViewDropDown}}" stepKey="waitForWebsiteAreVisible"/> + <seeElement selector="{{AdminProductGridFilterSection.storeViewOptions(storeViewId)}}" stepKey="seeStoreViewOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewFillSortOrderActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewFillSortOrderActionGroup.xml new file mode 100644 index 0000000000000..1b9b147209c66 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminCreateStoreViewFillSortOrderActionGroup.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="AdminCreateStoreViewFillSortOrderActionGroup" extends="AdminCreateStoreViewActionGroup"> + <annotations> + <description>Fill 'Sort Order' field</description> + </annotations> + <arguments> + <argument name="sortOrder" type="string" defaultValue="0"/> + </arguments> + + <fillField selector="{{AdminNewStoreSection.sortOrderTextField}}" userInput="{{sortOrder}}" stepKey="fillSortOrder" after="enterStoreViewCode"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml index bdb1842cf2959..39664ae10a07d 100644 --- a/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml +++ b/app/code/Magento/Store/Test/Mftf/Data/StoreData.xml @@ -206,4 +206,23 @@ <data key="store_type">store</data> <data key="store_action">add</data> </entity> + <!--Stores views with same name--> + <entity name="customStoreViewSameNameFirst" type="store"> + <data key="name">sameNameStoreView</data> + <data key="code" unique="suffix">storeViewCode</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="customStoreViewSameNameSecond" type="store"> + <data key="name">sameNameStoreView</data> + <data key="code" unique="suffix">storeViewCode</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> </entities> diff --git a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml index e56836c491276..cd7f180d0bb0e 100644 --- a/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml +++ b/app/code/Magento/Store/Test/Mftf/Section/AdminStoresGridSection/AdminStoresGridSection.xml @@ -22,5 +22,6 @@ <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"/> + <element name="storeViewLinkInNthRow" type="text" selector="tr:nth-of-type({{row}}) > .col-store_title > a" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateDuplicateNameStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateDuplicateNameStoreViewTest.xml new file mode 100644 index 0000000000000..ec81424b1acfa --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateDuplicateNameStoreViewTest.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="AdminCreateDuplicateNameStoreViewTest"> + <annotations> + <features value="Store"/> + <stories value="Create a store view in admin"/> + <title value="Admin should be able to create a Store View with the same name"/> + <description value="Admin should be able to create a Store View with the same name"/> + <group value="storeView"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-36863"/> + </annotations> + <before> + <!--Create two store views with same name, but different codes--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="customStoreViewSameNameFirst"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="_defaultStoreGroup"/> + <argument name="customStore" value="customStoreViewSameNameSecond"/> + </actionGroup> + </before> + <after> + <!--Delete both store views--> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteFirstStoreView"> + <argument name="customStore" value="customStoreViewSameNameFirst"/> + </actionGroup> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteSecondStoreView"> + <argument name="customStore" value="customStoreViewSameNameSecond"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Get Id of store views--> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoreViews"/> + <click selector="{{AdminStoresGridSection.storeViewLinkInNthRow('2')}}" stepKey="openFirstViewPAge" /> + <grabFromCurrentUrl stepKey="getStoreViewIdFirst" regex="~/store_id/(\d+)/~"/> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoreViewsAgain"/> + <click selector="{{AdminStoresGridSection.storeViewLinkInNthRow('3')}}" stepKey="openSecondViewPAge" /> + <grabFromCurrentUrl stepKey="getStoreViewIdSecond" regex="~/store_id/(\d+)/~"/> + <!--Go to catalog -> product grid, open the filter and check the listed store view--> + <actionGroup ref="AdminCheckStoreViewOptionsActionGroup" stepKey="checkFirstStoreView"> + <argument name="storeViewId" value="{$getStoreViewIdFirst}"/> + </actionGroup> + <actionGroup ref="AdminCheckStoreViewOptionsActionGroup" stepKey="checkSecondStoreView"> + <argument name="storeViewId" value="{$getStoreViewIdSecond}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.xml new file mode 100644 index 0000000000000..442ee99e12793 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/Test/StorefrontCheckSortOrderStoreViewTest.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="StorefrontCheckSortOrderStoreView"> + <annotations> + <features value="Backend"/> + <stories value="Github issue: #13401 'Store View' sort order values are not reflected"/> + <title value="Check 'Store view' sort order values"/> + <description value="Check 'Store View' sort order values no frontend store-switcher"/> + <severity value="MINOR"/> + <group value="store"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createFirstStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore"> + <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="deleteCustomStore"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </actionGroup> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteSecondStore"> + <argument name="storeGroupName" value="SecondStoreGroupUnique.name"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </after> + <actionGroup ref="AdminCreateStoreViewFillSortOrderActionGroup" stepKey="createFirstStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreGroup"/> + <argument name="sortOrder" value="30"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewFillSortOrderActionGroup" stepKey="createSecondStoreView"> + <argument name="StoreGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreGroupUnique"/> + <argument name="sortOrder" value="20"/> + </actionGroup> + + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> + <click stepKey="selectStoreSwitcher" selector="{{StorefrontFooterSection.switchStoreButton}}"/> + <grabTextFrom selector="{{StorefrontFooterSection.storeViewOptionNumber('1')}}" stepKey="grabSwatchFirstOption"/> + <grabTextFrom selector="{{StorefrontFooterSection.storeViewOptionNumber('2')}}" stepKey="grabSwatchSecondOption"/> + <assertStringContainsString stepKey="checkingSwatchFirstOption"> + <expectedResult type="string">{{SecondStoreGroupUnique.name}}</expectedResult> + <actualResult type="variable">$grabSwatchFirstOption</actualResult> + </assertStringContainsString> + <assertStringContainsString stepKey="checkingSwatchSecondOption"> + <expectedResult type="string">{{customStoreGroup.name}}</expectedResult> + <actualResult type="variable">$grabSwatchSecondOption</actualResult> + </assertStringContainsString> + </test> +</tests> diff --git a/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php b/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php index 9106da8ffb177..60c69834f6aa6 100644 --- a/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php +++ b/app/code/Magento/Store/Test/Unit/Block/SwitcherTest.php @@ -7,91 +7,159 @@ namespace Magento\Store\Test\Unit\Block; +use Magento\Directory\Helper\Data; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Data\Helper\PostHelper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\UrlInterface; use Magento\Framework\View\Element\Template\Context; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Block\Switcher; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Website; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class SwitcherTest extends TestCase { - /** @var Switcher */ - protected $switcher; - - /** @var Context|MockObject */ - protected $context; + /** + * @var Switcher + */ + private $switcher; - /** @var PostHelper|MockObject */ - protected $corePostDataHelper; + /** + * @var PostHelper|MockObject + */ + private $corePostDataHelperMock; - /** @var StoreManagerInterface|MockObject */ - protected $storeManager; + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; - /** @var UrlInterface|MockObject */ - protected $urlBuilder; + /** + * @var UrlInterface|MockObject + */ + private $urlBuilderMock; - /** @var StoreInterface|MockObject */ - private $store; + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; /** * @return void */ protected function setUp(): void { - $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) - ->getMock(); - $this->urlBuilder = $this->getMockForAbstractClass(UrlInterface::class); - $this->context = $this->createMock(Context::class); - $this->context->expects($this->any())->method('getStoreManager')->willReturn($this->storeManager); - $this->context->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilder); - $this->corePostDataHelper = $this->createMock(PostHelper::class); - $this->store = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); + $this->urlBuilderMock = $this->createMock(UrlInterface::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $contextMock = $this->createMock(Context::class); + $contextMock->method('getStoreManager')->willReturn($this->storeManagerMock); + $contextMock->method('getUrlBuilder')->willReturn($this->urlBuilderMock); + $contextMock->method('getScopeConfig')->willReturn($this->scopeConfigMock); + $this->corePostDataHelperMock = $this->createMock(PostHelper::class); $this->switcher = (new ObjectManager($this))->getObject( Switcher::class, [ - 'context' => $this->context, - 'postDataHelper' => $this->corePostDataHelper, + 'context' => $contextMock, + 'postDataHelper' => $this->corePostDataHelperMock, ] ); } + public function testGetStoresSortOrder() + { + $groupId = 1; + $storesSortOrder = [ + 1 => 2, + 2 => 4, + 3 => 1, + 4 => 3 + ]; + + $currentStoreMock = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $currentStoreMock->method('getGroupId')->willReturn($groupId); + $currentStoreMock->method('isUseStoreInUrl')->willReturn(false); + $this->storeManagerMock->method('getStore') + ->willReturn($currentStoreMock); + + $currentWebsiteMock = $this->getMockBuilder(Website::class) + ->disableOriginalConstructor() + ->getMock(); + $this->storeManagerMock->method('getWebsite') + ->willReturn($currentWebsiteMock); + + $stores = []; + foreach ($storesSortOrder as $storeId => $sortOrder) { + $storeMock = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->setMethods(['getId', 'getGroupId', 'getSortOrder', 'isActive', 'getUrl']) + ->getMock(); + $storeMock->method('getId')->willReturn($storeId); + $storeMock->method('getGroupId')->willReturn($groupId); + $storeMock->method('getSortOrder')->willReturn($sortOrder); + $storeMock->method('isActive')->willReturn(true); + $storeMock->method('getUrl')->willReturn('https://example.org'); + $stores[] = $storeMock; + } + + $scopeConfigMap = array_map(static function ($item) { + return [ + Data::XML_PATH_DEFAULT_LOCALE, + ScopeInterface::SCOPE_STORE, + $item, + 'en_US' + ]; + }, $stores); + $this->scopeConfigMock->method('getValue') + ->willReturnMap($scopeConfigMap); + + $currentWebsiteMock->method('getStores') + ->willReturn($stores); + + $this->assertEquals([3, 1, 4, 2], array_keys($this->switcher->getStores())); + } + /** * @return void */ public function testGetTargetStorePostData() { - $store = $this->getMockBuilder(Store::class) + $storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->getMock(); - $store->expects($this->any()) - ->method('getCode') + $oldStoreMock = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $storeMock->method('getCode') ->willReturn('new-store'); $storeSwitchUrl = 'http://domain.com/stores/store/redirect'; - $store->expects($this->atLeastOnce()) + $storeMock->expects($this->atLeastOnce()) ->method('getCurrentUrl') ->with(false) ->willReturn($storeSwitchUrl); - $this->storeManager->expects($this->once()) + $this->storeManagerMock->expects($this->once()) ->method('getStore') - ->willReturn($this->store); - $this->store->expects($this->once()) + ->willReturn($oldStoreMock); + $oldStoreMock->expects($this->once()) ->method('getCode') ->willReturn('old-store'); - $this->urlBuilder->expects($this->once()) + $this->urlBuilderMock->expects($this->once()) ->method('getUrl') ->willReturn($storeSwitchUrl); - $this->corePostDataHelper->expects($this->any()) - ->method('getPostData') + $this->corePostDataHelperMock->method('getPostData') ->with($storeSwitchUrl, ['___store' => 'new-store', 'uenc' => null, '___from_store' => 'old-store']); - $this->switcher->getTargetStorePostData($store); + $this->switcher->getTargetStorePostData($storeMock); } /** @@ -104,7 +172,7 @@ public function testIsStoreInUrl($isUseStoreInUrl) $storeMock->expects($this->once())->method('isUseStoreInUrl')->willReturn($isUseStoreInUrl); - $this->storeManager->expects($this->any())->method('getStore')->willReturn($storeMock); + $this->storeManagerMock->method('getStore')->willReturn($storeMock); $this->assertEquals($this->switcher->isStoreInUrl(), $isUseStoreInUrl); // check value is cached $this->assertEquals($this->switcher->isStoreInUrl(), $isUseStoreInUrl); @@ -114,7 +182,7 @@ public function testIsStoreInUrl($isUseStoreInUrl) * @see self::testIsStoreInUrlDataProvider() * @return array */ - public function isStoreInUrlDataProvider() + public function isStoreInUrlDataProvider(): array { return [[true], [false]]; } diff --git a/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php b/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php index 907eb74e20fa2..f8aa09cb20a61 100644 --- a/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php +++ b/app/code/Magento/Store/Ui/Component/Listing/Column/Store/Options.php @@ -10,7 +10,7 @@ use Magento\Store\Model\System\Store as SystemStore; /** - * Class Options + * Ui stores options */ class Options implements OptionSourceInterface { @@ -93,37 +93,38 @@ protected function sanitizeName($name) * * @return void */ - protected function generateCurrentOptions() + protected function generateCurrentOptions(): void { $websiteCollection = $this->systemStore->getWebsiteCollection(); $groupCollection = $this->systemStore->getGroupCollection(); $storeCollection = $this->systemStore->getStoreCollection(); - /** @var \Magento\Store\Model\Website $website */ + foreach ($websiteCollection as $website) { $groups = []; - /** @var \Magento\Store\Model\Group $group */ foreach ($groupCollection as $group) { - if ($group->getWebsiteId() == $website->getId()) { + if ($group->getWebsiteId() === $website->getId()) { $stores = []; - /** @var \Magento\Store\Model\Store $store */ foreach ($storeCollection as $store) { - if ($store->getGroupId() == $group->getId()) { - $name = $this->sanitizeName($store->getName()); - $stores[$name]['label'] = str_repeat(' ', 8) . $name; - $stores[$name]['value'] = $store->getId(); + if ($store->getGroupId() === $group->getId()) { + $stores[] = [ + 'label' => str_repeat(' ', 8) . $this->sanitizeName($store->getName()), + 'value' => $store->getId(), + ]; } } if (!empty($stores)) { - $name = $this->sanitizeName($group->getName()); - $groups[$name]['label'] = str_repeat(' ', 4) . $name; - $groups[$name]['value'] = array_values($stores); + $groups[] = [ + 'label' => str_repeat(' ', 4) . $this->sanitizeName($group->getName()), + 'value' => array_values($stores), + ]; } } } if (!empty($groups)) { - $name = $this->sanitizeName($website->getName()); - $this->currentOptions[$name]['label'] = $name; - $this->currentOptions[$name]['value'] = array_values($groups); + $this->currentOptions[] = [ + 'label' => $this->sanitizeName($website->getName()), + 'value' => array_values($groups), + ]; } } } diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml index d56572afd8847..07ce30b702f91 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminSetUpWatermarkForSwatchImageTest.xml @@ -38,8 +38,7 @@ </actionGroup> <!-- Select Edit next to the Default Store View --> <comment userInput="Select Edit next to the Default Store View" stepKey="commentEditDefaultView"/> - <click selector="{{AdminCustomerGridSection.firstRowEditLink}}" stepKey="clickToEditDefaultStoreView"/> - <waitForPageLoad stepKey="waitForDefaultStorePage"/> + <actionGroup ref="AdminClickFirstRowEditLinkOnCustomerGridActionGroup" stepKey="clickToEditDefaultStoreView"/> <!-- Expand the Product Image Watermarks section--> <comment userInput="Expand the Product Image Watermarks section" stepKey="commentOpenWatermarksSection"/> <click selector="{{AdminDesignConfigSection.watermarkSectionHeader}}" stepKey="clickToProductImageWatermarks"/> diff --git a/app/code/Magento/Tax/Pricing/Render/Adjustment.php b/app/code/Magento/Tax/Pricing/Render/Adjustment.php index 8613e62f2983e..0e5c619790a97 100644 --- a/app/code/Magento/Tax/Pricing/Render/Adjustment.php +++ b/app/code/Magento/Tax/Pricing/Render/Adjustment.php @@ -38,6 +38,8 @@ public function __construct( } /** + * Apply the right HTML output to the adjustment + * * @return string */ protected function apply() @@ -173,4 +175,16 @@ public function displayPriceExcludingTax() { return $this->taxHelper->displayPriceExcludingTax(); } + + /** + * Obtain a value for data-price-type attribute + * + * @return string + */ + public function getDataPriceType(): string + { + return $this->amountRender->getPriceType() === 'finalPrice' + ? 'basePrice' + : 'base' . ucfirst($this->amountRender->getPriceType()); + } } diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml index b9c3baab8c0dd..d2f1b1aa44393 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml @@ -79,8 +79,7 @@ <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You added"/> <!-- Fill in address for CA --> - <amOnPage url="{{CheckoutPage.url}}" stepKey="goToCheckout"/> - <waitForPageLoad stepKey="waitForShippingSection"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="goToCheckout"/> <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{Simple_US_Customer_CA.email}}" stepKey="enterEmail"/> <waitForLoadingMaskToDisappear stepKey="waitEmailLoad"/> <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml index 8fafbd9986c64..5441664d7c530 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml @@ -79,8 +79,7 @@ <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You added"/> <!-- Assert that taxes are applied correctly for CA --> - <amOnPage url="{{CheckoutPage.url}}" stepKey="goToCheckout"/> - <waitForPageLoad stepKey="waitForShippingSection"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="goToCheckout"/> <waitForElementVisible stepKey="waitForOverviewVisible" selector="{{CheckoutPaymentSection.tax}}"/> <see stepKey="seeTax2" selector="{{CheckoutPaymentSection.tax}}" userInput="$8.25"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml index ae988cd43efd5..76f4dcd8e161e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml @@ -94,8 +94,7 @@ <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You added"/> <!-- Assert that taxes are applied correctly for NY --> - <amOnPage url="{{CheckoutPage.url}}" stepKey="goToCheckout"/> - <waitForPageLoad stepKey="waitForShippingSection"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="goToCheckout"/> <see stepKey="seeAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{SimpleTaxNY.state}}"/> <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml index 6552d31a8a523..c98765976f36f 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml @@ -94,8 +94,7 @@ <see stepKey="seeSuccess" selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You added"/> <!-- Assert that taxes are applied correctly for NY --> - <amOnPage url="{{CheckoutPage.url}}" stepKey="goToCheckout"/> - <waitForPageLoad stepKey="waitForShippingSection"/> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="goToCheckout"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <see stepKey="seeAddress" selector="{{CheckoutShippingSection.defaultShipping}}" userInput="{{SimpleTaxNY.state}}"/> diff --git a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml index e87d1c9eb96aa..685893151bc5a 100644 --- a/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml +++ b/app/code/Magento/Tax/view/base/templates/pricing/adjustment.phtml @@ -6,12 +6,13 @@ ?> <?php /** @var \Magento\Tax\Pricing\Render\Adjustment $block */ ?> +<?php /** @var $escaper \Magento\Framework\Escaper */ ?> -<?php if ($block->displayBothPrices()) : ?> - <span id="<?= $block->escapeHtmlAttr($block->buildIdWithPrefix('price-excluding-tax-')) ?>" - data-label="<?= $block->escapeHtmlAttr(__('Excl. Tax')) ?>" +<?php if ($block->displayBothPrices()): ?> + <span id="<?= $escaper->escapeHtmlAttr($block->buildIdWithPrefix('price-excluding-tax-')) ?>" + data-label="<?= $escaper->escapeHtmlAttr(__('Excl. Tax')) ?>" data-price-amount="<?= /* @noEscape */ $block->getRawAmount() ?>" - data-price-type="basePrice" + data-price-type="<?= $escaper->escapeHtmlAttr($block->getDataPriceType()); ?>" class="price-wrapper price-excluding-tax"> <span class="price"><?= /* @noEscape */ $block->getDisplayAmountExclTax() ?></span></span> <?php endif; ?> diff --git a/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml b/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.xml new file mode 100644 index 0000000000000..c60385b768bf3 --- /dev/null +++ b/app/code/Magento/Theme/Test/Mftf/Test/StoreFrontCheckNotificationMessageContainerTest.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="StoreFrontCheckNotificationMessageContainerTest"> + <annotations> + <features value="Message container"/> + <stories value="Message container selector"/> + <title value="Check notification message container"/> + <description value="Check aria-atomic property on notification container message"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-37339"/> + <group value="Theme"/> + </annotations> + <before> + <createData entity="SimpleProduct2" stepKey="simpleProduct"/> + <createData entity="SalesRuleSpecificCouponAndByPercent" stepKey="createSalesRule"/> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createSalesRule"/> + </createData> + </before> + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> + </after> + + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> + <argument name="product" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="addProductToTheCart"> + <argument name="productQty" value="1"/> + </actionGroup> + + <waitForElementVisible selector="{{StorefrontProductPageSection.alertMessage}}[aria-atomic=true]" stepKey="checkAddedToCartMessage"/> + + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShipping"> + <argument name="shippingMethod" value="Flat Rate"/> + </actionGroup> + <actionGroup ref="StorefrontApplyDiscountCodeActionGroup" stepKey="applyCoupon"> + <argument name="discountCode" value="$createCouponForCartPriceRule.code$"/> + </actionGroup> + + <waitForElementVisible selector="{{DiscountSection.DiscountVerificationMsgWithAriaAtomicProperty}}" stepKey="checkCouponCodeApply"/> + </test> +</tests> diff --git a/app/code/Magento/Theme/view/frontend/templates/messages.phtml b/app/code/Magento/Theme/view/frontend/templates/messages.phtml index 85e752635fb3a..f863da70e8987 100644 --- a/app/code/Magento/Theme/view/frontend/templates/messages.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/messages.phtml @@ -6,7 +6,7 @@ ?> <div data-bind="scope: 'messages'"> <!-- ko if: cookieMessages && cookieMessages.length > 0 --> - <div role="alert" data-bind="foreach: { data: cookieMessages, as: 'message' }" class="messages"> + <div aria-atomic="true" role="alert" data-bind="foreach: { data: cookieMessages, as: 'message' }" class="messages"> <div data-bind="attr: { class: 'message-' + message.type + ' ' + message.type + ' message', 'data-ui-id': 'message-' + message.type @@ -17,7 +17,9 @@ <!-- /ko --> <!-- ko if: messages().messages && messages().messages.length > 0 --> - <div role="alert" data-bind="foreach: { data: messages().messages, as: 'message' }" class="messages"> + <div aria-atomic="true" role="alert" class="messages" data-bind="foreach: { + data: messages().messages, as: 'message' + }"> <div data-bind="attr: { class: 'message-' + message.type + ' ' + message.type + ' message', 'data-ui-id': 'message-' + message.type diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml index 01b2101d3346c..ddb6c4071a0e7 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml @@ -34,8 +34,7 @@ <selectOption selector="{{ContentManagementSection.Switcher}}" userInput="TinyMCE 4" stepKey="switchToVersion4" /> <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions1" /> <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig1" /> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage1"/> - <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage1"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle1"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab1" /> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4"/> @@ -62,8 +61,7 @@ <selectOption selector="{{ContentManagementSection.Switcher}}" userInput="TinyMCE 3" stepKey="switchToVersion3" /> <click selector="{{ContentManagementSection.WYSIWYGOptions}}" stepKey="collapseWYSIWYGOptions2" /> <click selector="{{ContentManagementSection.Save}}" stepKey="clickSaveConfig2" /> - <amOnPage url="{{CmsNewPagePage.url}}" stepKey="navigateToPage2"/> - <waitForPageLoad stepKey="wait5"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToPage2"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle2"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab2" /> <comment userInput="removing deprecated element" stepKey="waitForTinyMCE3"/> diff --git a/app/code/Magento/Translation/view/frontend/requirejs-config.js b/app/code/Magento/Translation/view/frontend/requirejs-config.js index b4b3ce0f8c554..9a99d49eddbcf 100644 --- a/app/code/Magento/Translation/view/frontend/requirejs-config.js +++ b/app/code/Magento/Translation/view/frontend/requirejs-config.js @@ -10,8 +10,5 @@ var config = { addClass: 'Magento_Translation/js/add-class', 'Magento_Translation/add-class': 'Magento_Translation/js/add-class' } - }, - deps: [ - 'mage/translate-inline' - ] + } }; diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.xml new file mode 100644 index 0000000000000..c7236c33e7cc0 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest.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="AdminGridFilterRemoveErrorMessageBeforeApplyFiltersTest"> + <annotations> + <stories value="Reset Error Messages"/> + <title value="Remove Error Message Before Apply Filters"/> + <description value="Test login to Admin UI and Remove Error Message Before Apply Filters"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37450"/> + <group value="ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="NewRootCategory" stepKey="rootCategory"/> + <createData entity="defaultSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="rootCategory" /> + </createData> + <createData entity="defaultSimpleProduct" stepKey="createProduct2"> + <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 second store --> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{customWebsite.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> + </before> + <after> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteProduct2" createDataKey="createProduct2"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Filter created simple product in grid and add category and website created in create data--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowOfCreatedSimpleProduct"/> + <waitForPageLoad stepKey="waitUntilProductIsOpened"/> + <actionGroup ref="AddWebsiteToProductActionGroup" stepKey="updateSimpleProductAddingWebsiteCreated"> + <argument name="website" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Search updated simple product(from above step) in the grid by StoreView and Name--> + <actionGroup ref="FilterProductInGridByStoreViewAndNameActionGroup" stepKey="searchCreatedSimpleProductInGrid"> + <argument name="storeView" value="{{customStoreEN.name}}"/> + <argument name="productName" value="$$createProduct2.name$$"/> + </actionGroup> + + <!--Go to stores and delete website created in create data--> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + + <!--Go to grid page and verify AssertErrorMessage--> + <actionGroup ref="AssertErrorMessageAfterDeletingWebsiteActionGroup" stepKey="verifyErrorMessage"> + <argument name="errorMessage" value="Something went wrong with processing the default view and we have restored the filter to its original state."/> + </actionGroup> + + <!--Apply new filters to verify error message is removed --> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> + <click selector="{{AdminProductGridFilterSection.storeViewDropdown('Default Store View')}}" stepKey="clickStoreViewDropdown"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="$$createProduct.name$$" stepKey="fillProductNameInNameFilter"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <see selector="{{AdminProductGridFilterSection.nthRow('1')}}" userInput="$$createProduct.name$$" stepKey="seeFirstRowToVerifyProductVisibleInGrid"/> + <dontSeeElement selector="{{AdminMessagesSection.error}}" stepKey="dontSeeErrorMessage"/> + + </test> +</tests> 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 5f29c5982e094..0ac35df78e001 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 @@ -1126,13 +1126,17 @@ define([ * Update whether value differs from default value */ setDifferedFromDefault: function () { - var recordData = utils.copy(this.recordData()); + var recordData; - Array.isArray(recordData) && recordData.forEach(function (item) { - delete item['record_id']; - }); + if (this.default) { + recordData = utils.copy(this.recordData()); + + Array.isArray(recordData) && recordData.forEach(function (item) { + delete item['record_id']; + }); - this.isDifferedFromDefault(!_.isEqual(recordData, this.default)); + this.isDifferedFromDefault(!_.isEqual(recordData, this.default)); + } }, /** 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 9a34e57df86c7..65443fadf8007 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 @@ -668,7 +668,7 @@ define([ * @returns {Object} Chainable */ toggleListVisible: function () { - this.listVisible(!this.listVisible()); + this.listVisible(!this.disabled() && !this.listVisible()); return this; }, diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js index d675bd7a60ab5..7dcf0994ef56b 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -2,6 +2,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +/* eslint-disable no-undef */ define([ 'jquery', 'Magento_Ui/js/grid/columns/column', @@ -32,7 +33,8 @@ define([ listens: { '${ $.provider }:params.filters': 'hide', '${ $.provider }:params.search': 'hide', - '${ $.provider }:params.paging': 'hide' + '${ $.provider }:params.paging': 'hide', + '${ $.provider }:data.items': 'updateDisplayedRecord' }, exports: { height: '${ $.parentName }.thumbnail_url:previewHeight' @@ -48,6 +50,25 @@ define([ this._super(); $(document).on('keydown', this.handleKeyDown.bind(this)); + this.lastOpenedImage.subscribe(function (newValue) { + + if (newValue === false && _.isNull(this.visibleRecord())) { + return; + } + + if (newValue === this.visibleRecord()) { + return; + } + + if (newValue === false) { + this.hide(); + + return; + } + + this.show(this.masonry().rows()[newValue]); + }.bind(this)); + return this; }, @@ -128,8 +149,6 @@ define([ * @param {Object} record */ show: function (record) { - var img; - if (record._rowIndex === this.visibleRecord()) { this.hide(); @@ -141,9 +160,21 @@ define([ this._selectRow(record.rowNumber || null); this.visibleRecord(record._rowIndex); - img = $(this.previewImageSelector + ' img'); + this.lastOpenedImage(record._rowIndex); + this.updateImageData(); + }, - if (img.get(0).complete) { + /** + * Update image data when image preview is opened + */ + updateImageData: function () { + var img = $(this.previewImageSelector + ' img'); + + if (!img.get(0)) { + setTimeout(function () { + this.updateImageData(); + }.bind(this), 100); + } else if (img.get(0).complete) { this.updateHeight(); this.scrollToPreview(); } else { @@ -152,8 +183,17 @@ define([ this.scrollToPreview(); }.bind(this)); } + }, - this.lastOpenedImage(record._rowIndex); + /** + * Update preview displayed record data from the new items data if the preview is expanded + * + * @param {Array} items + */ + updateDisplayedRecord: function (items) { + if (!_.isNull(this.visibleRecord())) { + this.displayedRecord(items[this.visibleRecord()]); + } }, /** diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js new file mode 100644 index 0000000000000..a913f3fa4a042 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js @@ -0,0 +1,88 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/ui-select', + 'jquery', + 'underscore' +], function (Select, $, _) { + 'use strict'; + + return Select.extend({ + defaults: { + bookmarkProvider: 'ns = ${ $.ns }, index = bookmarks', + filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', + validationUrl: false, + loadedOption: [], + validationLoading: true, + imports: { + activeIndex: '${ $.bookmarkProvider }:activeIndex' + }, + modules: { + filterChips: '${ $.filterChipsProvider }' + }, + listens: { + activeIndex: 'validateInitialValue' + } + + }, + + /** + * Initializes UiSelect component. + * + * @returns {UiSelect} Chainable. + */ + initialize: function () { + this._super(); + + this.validateInitialValue(); + + return this; + }, + + /** + * Validate initial value actually exists + */ + validateInitialValue: function () { + if (_.isEmpty(this.value())) { + this.validationLoading(false); + + return; + } + + $.ajax({ + url: this.validationUrl, + type: 'GET', + dataType: 'json', + context: this, + data: { + ids: this.value() + }, + + /** @param {Object} response */ + success: function (response) { + if (!_.isEmpty(response)) { + this.options([]); + this.success({ + options: response + }); + } + this.filterChips().updateActive(); + }, + + /** set empty array if error occurs */ + error: function () { + this.options([]); + }, + + /** stop loader */ + complete: function () { + this.validationLoading(false); + this.setCaption(); + } + }); + } + }); +}); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js index fe33389eabad4..848ad60219a2b 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/filters.js @@ -200,6 +200,7 @@ define([ * @returns {Filters} Chainable. */ apply: function () { + $('body').notification('clear'); this.set('applied', removeEmpty(this.filters)); return this; diff --git a/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js index 1f870e9e819a1..3c5e72d4d66ed 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js @@ -5,17 +5,20 @@ define([ 'uiComponent', - 'underscore' -], function (Component, _) { + 'underscore', + 'jquery' +], function (Component, _, $) { 'use strict'; return Component.extend({ defaults: { listingNamespace: null, + bookmarkProvider: 'componentType = bookmark, ns = ${ $.listingNamespace }', filterProvider: 'componentType = filters, ns = ${ $.listingNamespace }', filterKey: 'filters', searchString: location.search, modules: { + bookmarks: '${ $.bookmarkProvider }', filterComponent: '${ $.filterProvider }' } }, @@ -36,7 +39,9 @@ define([ * Apply filter */ apply: function () { - var urlFilter = this.getFilterParam(this.searchString); + var urlFilter = this.getFilterParam(this.searchString), + applied, + filters; if (_.isUndefined(this.filterComponent())) { setTimeout(function () { @@ -46,9 +51,20 @@ define([ return; } + if (!_.isUndefined(this.bookmarks())) { + if (!_.size(this.bookmarks().getViewData(this.bookmarks().defaultIndex))) { + setTimeout(function () { + this.apply(); + }.bind(this), 500); + + return; + } + } + if (Object.keys(urlFilter).length) { - this.filterComponent().setData(urlFilter, false); - this.filterComponent().apply(); + applied = this.filterComponent().get('applied'); + filters = $.extend({}, applied, urlFilter); + this.filterComponent().set('applied', filters); } }, diff --git a/app/code/Magento/Ui/view/frontend/web/template/messages.html b/app/code/Magento/Ui/view/frontend/web/template/messages.html index 0a8f672765b3c..c094d9d58bb75 100644 --- a/app/code/Magento/Ui/view/frontend/web/template/messages.html +++ b/app/code/Magento/Ui/view/frontend/web/template/messages.html @@ -6,12 +6,12 @@ --> <div data-role="checkout-messages" class="messages" data-bind="visible: isVisible(), click: removeAll"> <!-- ko foreach: messageContainer.getErrorMessages() --> - <div role="alert" class="message message-error error"> + <div aria-atomic="true" role="alert" class="message message-error error"> <div data-ui-id="checkout-cart-validationmessages-message-error" data-bind="text: $data"></div> </div> <!--/ko--> <!-- ko foreach: messageContainer.getSuccessMessages() --> - <div role="alert" class="message message-success success"> + <div aria-atomic="true" role="alert" class="message message-success success"> <div data-ui-id="checkout-cart-validationmessages-message-success" data-bind="text: $data"></div> </div> <!--/ko--> diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index 195c3f397ff18..b05b70cfcbc71 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -5,6 +5,16 @@ */ namespace Magento\Widget\Model; +use Magento\Framework\App\Cache\Type\Config; +use Magento\Framework\DataObject; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Asset\Repository; +use Magento\Framework\View\Asset\Source; +use Magento\Framework\View\FileSystem; +use Magento\Widget\Helper\Conditions; +use Magento\Widget\Model\Config\Data; + /** * Widget model for different purposes * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -15,32 +25,32 @@ class Widget { /** - * @var \Magento\Widget\Model\Config\Data + * @var Data */ protected $dataStorage; /** - * @var \Magento\Framework\App\Cache\Type\Config + * @var Config */ protected $configCacheType; /** - * @var \Magento\Framework\View\Asset\Repository + * @var Repository */ protected $assetRepo; /** - * @var \Magento\Framework\View\Asset\Source + * @var Source */ protected $assetSource; /** - * @var \Magento\Framework\View\FileSystem + * @var FileSystem */ protected $viewFileSystem; /** - * @var \Magento\Framework\Escaper + * @var Escaper */ protected $escaper; @@ -50,30 +60,35 @@ class Widget protected $widgetsArray = []; /** - * @var \Magento\Widget\Helper\Conditions + * @var Conditions */ protected $conditionsHelper; /** - * @var \Magento\Framework\Math\Random + * @var Random */ private $mathRandom; /** - * @param \Magento\Framework\Escaper $escaper - * @param \Magento\Widget\Model\Config\Data $dataStorage - * @param \Magento\Framework\View\Asset\Repository $assetRepo - * @param \Magento\Framework\View\Asset\Source $assetSource - * @param \Magento\Framework\View\FileSystem $viewFileSystem - * @param \Magento\Widget\Helper\Conditions $conditionsHelper + * @var string[] + */ + private $reservedChars = ['}', '{']; + + /** + * @param Escaper $escaper + * @param Data $dataStorage + * @param Repository $assetRepo + * @param Source $assetSource + * @param FileSystem $viewFileSystem + * @param Conditions $conditionsHelper */ public function __construct( - \Magento\Framework\Escaper $escaper, - \Magento\Widget\Model\Config\Data $dataStorage, - \Magento\Framework\View\Asset\Repository $assetRepo, - \Magento\Framework\View\Asset\Source $assetSource, - \Magento\Framework\View\FileSystem $viewFileSystem, - \Magento\Widget\Helper\Conditions $conditionsHelper + Escaper $escaper, + Data $dataStorage, + Repository $assetRepo, + Source $assetSource, + FileSystem $viewFileSystem, + Conditions $conditionsHelper ) { $this->escaper = $escaper; $this->dataStorage = $dataStorage; @@ -110,14 +125,11 @@ public function getWidgetByClassType($type) $widgets = $this->getWidgets(); /** @var array $widget */ foreach ($widgets as $widget) { - if (isset($widget['@'])) { - if (isset($widget['@']['type'])) { - if ($type === $widget['@']['type']) { - return $widget; - } - } + if (isset($widget['@']['type']) && $type === $widget['@']['type']) { + return $widget; } } + return null; } @@ -131,6 +143,7 @@ public function getWidgetByClassType($type) */ public function getConfigAsXml($type) { + // phpstan:ignore return $this->getXmlElementByType($type); } @@ -296,42 +309,70 @@ public function getWidgetsArray($filters = []) */ public function getWidgetDeclaration($type, $params = [], $asIs = true) { - $directive = '{{widget type="' . $type . '"'; $widget = $this->getConfigAsObject($type); + $params = array_filter($params, function ($value) { + return $value !== null && $value !== ''; + }); + + $directiveParams = ''; foreach ($params as $name => $value) { // Retrieve default option value if pre-configured - if ($name == 'conditions') { - $name = 'conditions_encoded'; - $value = $this->conditionsHelper->encode($value); - } elseif (is_array($value)) { - $value = implode(',', $value); - } elseif (trim($value) == '') { - $parameters = $widget->getParameters(); - if (isset($parameters[$name]) && is_object($parameters[$name])) { - $value = $parameters[$name]->getValue(); - } - } - if (isset($value)) { - $directive .= sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)); - } + $directiveParams .= $this->getDirectiveParam($widget, $name, $value); } - $directive .= $this->getWidgetPageVarName($params); - - $directive .= '}}'; + $directive = sprintf('{{widget type="%s"%s%s}}', $type, $directiveParams, $this->getWidgetPageVarName($params)); if ($asIs) { return $directive; } - $html = sprintf( + return sprintf( '<img id="%s" src="%s" title="%s">', $this->idEncode($directive), $this->getPlaceholderImageUrl($type), $this->escaper->escapeUrl($directive) ); - return $html; + } + + /** + * Returns directive param with prepared value + * + * @param DataObject $widget + * @param string $name + * @param string|array $value + * @return string + */ + private function getDirectiveParam(DataObject $widget, string $name, $value): string + { + if ($name === 'conditions') { + $name = 'conditions_encoded'; + $value = $this->conditionsHelper->encode($value); + } elseif (is_array($value)) { + $value = implode(',', $value); + } elseif (trim($value) === '') { + $parameters = $widget->getParameters(); + if (isset($parameters[$name]) && is_object($parameters[$name])) { + $value = $parameters[$name]->getValue(); + } + } else { + $value = $this->getPreparedValue($value); + } + + return sprintf(' %s="%s"', $name, $this->escaper->escapeHtmlAttr($value, false)); + } + + /** + * Returns encoded value if it contains reserved chars + * + * @param string $value + * @return string + */ + private function getPreparedValue(string $value): string + { + $pattern = sprintf('/%s/', implode('|', $this->reservedChars)); + + return preg_match($pattern, $value) ? rawurlencode($value) : $value; } /** diff --git a/app/code/Magento/Wishlist/Controller/Shared/Allcart.php b/app/code/Magento/Wishlist/Controller/Shared/Allcart.php index 6300b14dcf515..89413eff8323f 100644 --- a/app/code/Magento/Wishlist/Controller/Shared/Allcart.php +++ b/app/code/Magento/Wishlist/Controller/Shared/Allcart.php @@ -3,13 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Wishlist\Controller\Shared; +use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; -use Magento\Wishlist\Model\ItemCarrier; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Forward; +use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; +use Magento\Wishlist\Model\ItemCarrier; -class Allcart extends \Magento\Framework\App\Action\Action +/** + * Wishlist Allcart Controller + */ +class Allcart extends Action implements HttpGetActionInterface, HttpPostActionInterface { /** * @var WishlistProvider @@ -17,7 +28,7 @@ class Allcart extends \Magento\Framework\App\Action\Action protected $wishlistProvider; /** - * @var \Magento\Wishlist\Model\ItemCarrier + * @var ItemCarrier */ protected $itemCarrier; @@ -39,21 +50,22 @@ public function __construct( /** * Add all items from wishlist to shopping cart * - * @return \Magento\Framework\Controller\ResultInterface + * {@inheritDoc} */ public function execute() { $wishlist = $this->wishlistProvider->getWishlist(); if (!$wishlist) { - /** @var \Magento\Framework\Controller\Result\Forward $resultForward */ + /** @var Forward $resultForward */ $resultForward = $this->resultFactory->create(ResultFactory::TYPE_FORWARD); $resultForward->forward('noroute'); return $resultForward; } $redirectUrl = $this->itemCarrier->moveAllToCart($wishlist, $this->getRequest()->getParam('qty')); - /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $resultRedirect->setUrl($redirectUrl); + return $resultRedirect; } } diff --git a/app/code/Magento/Wishlist/Controller/Shared/Cart.php b/app/code/Magento/Wishlist/Controller/Shared/Cart.php index c0a394ce9d762..939cbe3a2c46f 100644 --- a/app/code/Magento/Wishlist/Controller/Shared/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Shared/Cart.php @@ -13,7 +13,7 @@ use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context as ActionContext; use Magento\Framework\App\Action\HttpPostActionInterface; -use Magento\Framework\Controller\Result\Redirect; +use Magento\Framework\Controller\Result\Redirect as ResultRedirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\LocalizedException; @@ -124,9 +124,11 @@ public function execute() } catch (\Exception $e) { $this->messageManager->addExceptionMessage($e, __('We can\'t add the item to the cart right now.')); } - /** @var Redirect $resultRedirect */ + + /** @var ResultRedirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $resultRedirect->setUrl($redirectUrl); + return $resultRedirect; } } diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 5d9b1911bc292..7d30d958b5228 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -398,7 +398,11 @@ protected function _renderFiltersBefore() $availableProductTypes = $this->salesConfig->getAvailableProductTypes(); $this->getSelect()->join( ['cat_prod' => $this->getTable('catalog_product_entity')], - $this->getConnection()->quoteInto('cat_prod.type_id IN (?)', $availableProductTypes), + $this->getConnection() + ->quoteInto( + "cat_prod.type_id IN (?) AND {$mainTableName}.product_id = cat_prod.entity_id", + $availableProductTypes + ), [] ); } diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php index aef3cbf571ff6..622f072e8d668 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php @@ -24,7 +24,7 @@ class WishlistItemFactory public function create(array $data): WishlistItem { return new WishlistItem( - $data['quantity'], + $data['quantity'] ?? 0, $data['sku'] ?? null, $data['parent_sku'] ?? null, isset($data['wishlist_item_id']) ? (int) $data['wishlist_item_id'] : null, diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml index 28a17d30aea2b..a2219d5145f17 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml @@ -23,7 +23,6 @@ <createData entity="SimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> </before> <after> diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/AllcartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/AllcartTest.php index eea3346e8e81b..d9339af8144f4 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/AllcartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/AllcartTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Wishlist\Test\Unit\Controller\Shared; @@ -20,83 +21,60 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Wishlist\Controller\Shared\Allcart. + */ class AllcartTest extends TestCase { /** * @var Allcart */ - protected $allcartController; - - /** - * @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager - */ - protected $objectManagerHelper; - - /** - * @var Context - */ - protected $context; + private $allcartController; /** * @var WishlistProvider|MockObject */ - protected $wishlistProviderMock; + private $wishlistProviderMock; /** * @var ItemCarrier|MockObject */ - protected $itemCarrierMock; + private $itemCarrierMock; /** * @var Wishlist|MockObject */ - protected $wishlistMock; + private $wishlistMock; /** * @var Http|MockObject */ - protected $requestMock; - - /** - * @var ResultFactory|MockObject - */ - protected $resultFactoryMock; + private $requestMock; /** * @var Redirect|MockObject */ - protected $resultRedirectMock; + private $resultRedirectMock; /** * @var Forward|MockObject */ - protected $resultForwardMock; + private $resultForwardMock; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->wishlistProviderMock = $this->getMockBuilder(WishlistProvider::class) - ->disableOriginalConstructor() - ->getMock(); - $this->itemCarrierMock = $this->getMockBuilder(ItemCarrier::class) - ->disableOriginalConstructor() - ->getMock(); - $this->wishlistMock = $this->getMockBuilder(Wishlist::class) - ->disableOriginalConstructor() - ->getMock(); - $this->requestMock = $this->getMockBuilder(Http::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resultFactoryMock = $this->getMockBuilder(ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resultRedirectMock = $this->getMockBuilder(Redirect::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resultForwardMock = $this->getMockBuilder(Forward::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->resultFactoryMock->expects($this->any()) + $this->wishlistProviderMock = $this->createMock(WishlistProvider::class); + $this->itemCarrierMock = $this->createMock(ItemCarrier::class); + $this->wishlistMock = $this->createMock(Wishlist::class); + $this->requestMock = $this->createMock(Http::class); + $resultFactoryMock = $this->createMock(ResultFactory::class); + $this->resultRedirectMock = $this->createMock(Redirect::class); + $this->resultForwardMock = $this->createMock(Forward::class); + + $resultFactoryMock->expects($this->any()) ->method('create') ->willReturnMap( [ @@ -105,18 +83,18 @@ protected function setUp(): void ] ); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->context = $this->objectManagerHelper->getObject( + $objectManagerHelper = new ObjectManagerHelper($this); + $context = $objectManagerHelper->getObject( Context::class, [ 'request' => $this->requestMock, - 'resultFactory' => $this->resultFactoryMock + 'resultFactory' => $resultFactoryMock ] ); - $this->allcartController = $this->objectManagerHelper->getObject( + $this->allcartController = $objectManagerHelper->getObject( Allcart::class, [ - 'context' => $this->context, + 'context' => $context, 'wishlistProvider' => $this->wishlistProviderMock, 'itemCarrier' => $this->itemCarrierMock ] diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/CartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/CartTest.php index 923b33ef4748b..e6a127457a6c6 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/CartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Shared/CartTest.php @@ -8,6 +8,7 @@ namespace Magento\Wishlist\Test\Unit\Controller\Shared; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Exception; use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Checkout\Model\Cart; use Magento\Framework\App\Action\Context as ActionContext; @@ -29,156 +30,146 @@ use PHPUnit\Framework\TestCase; /** + * Test for \Magento\Wishlist\Controller\Shared\Cart. + * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CartTest extends TestCase { - /** @var SharedCart|MockObject */ - protected $model; - - /** @var RequestInterface|MockObject */ - protected $request; - - /** @var ManagerInterface|MockObject */ - protected $messageManager; - - /** @var ActionContext|MockObject */ - protected $context; - - /** @var Cart|MockObject */ - protected $cart; + /** + * @var SharedCart|MockObject + */ + private $model; - /** @var CartHelper|MockObject */ - protected $cartHelper; + /** + * @var RequestInterface|MockObject + */ + private $request; - /** @var Quote|MockObject */ - protected $quote; + /** + * @var ManagerInterface|MockObject + */ + private $messageManager; - /** @var OptionCollection|MockObject */ - protected $optionCollection; + /** + * @var Cart|MockObject + */ + private $cart; - /** @var OptionFactory|MockObject */ - protected $optionFactory; + /** + * @var CartHelper|MockObject + */ + private $cartHelper; - /** @var Option|MockObject */ - protected $option; + /** + * @var Quote|MockObject + */ + private $quote; - /** @var ItemFactory|MockObject */ - protected $itemFactory; + /** + * @var OptionCollection|MockObject + */ + private $optionCollection; - /** @var Item|MockObject */ - protected $item; + /** + * @var Option|MockObject + */ + private $option; - /** @var Escaper|MockObject */ - protected $escaper; + /** + * @var Item|MockObject + */ + private $item; - /** @var RedirectInterface|MockObject */ - protected $redirect; + /** + * @var Escaper|MockObject + */ + private $escaper; - /** @var ResultFactory|MockObject */ - protected $resultFactory; + /** + * @var RedirectInterface|MockObject + */ + private $redirect; - /** @var Redirect|MockObject */ - protected $resultRedirect; + /** + * @var Redirect|MockObject + */ + private $resultRedirect; - /** @var Product|MockObject */ - protected $product; + /** + * @var Product|MockObject + */ + private $product; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->request = $this->getMockBuilder(RequestInterface::class) - ->getMockForAbstractClass(); - - $this->redirect = $this->getMockBuilder(RedirectInterface::class) - ->getMockForAbstractClass(); - - $this->messageManager = $this->getMockBuilder(ManagerInterface::class) - ->getMockForAbstractClass(); - - $this->resultRedirect = $this->getMockBuilder(Redirect::class) - ->disableOriginalConstructor() - ->getMock(); + $this->request = $this->getMockForAbstractClass(RequestInterface::class); + $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class); + $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); + $this->resultRedirect = $this->createMock(Redirect::class); - $this->resultFactory = $this->getMockBuilder(ResultFactory::class) - ->disableOriginalConstructor() - ->getMock(); - $this->resultFactory->expects($this->once()) + $resultFactory = $this->createMock(ResultFactory::class); + $resultFactory->expects($this->once()) ->method('create') ->with(ResultFactory::TYPE_REDIRECT) ->willReturn($this->resultRedirect); - $this->context = $this->getMockBuilder(\Magento\Framework\App\Action\Context::class) + /** @var ActionContext|MockObject $context */ + $context = $this->getMockBuilder(ActionContext::class) ->disableOriginalConstructor() ->getMock(); - $this->context->expects($this->any()) + $context->expects($this->any()) ->method('getRequest') ->willReturn($this->request); - $this->context->expects($this->any()) + $context->expects($this->any()) ->method('getRedirect') ->willReturn($this->redirect); - $this->context->expects($this->any()) + $context->expects($this->any()) ->method('getMessageManager') ->willReturn($this->messageManager); - $this->context->expects($this->any()) + $context->expects($this->any()) ->method('getResultFactory') - ->willReturn($this->resultFactory); - - $this->cart = $this->getMockBuilder(\Magento\Checkout\Model\Cart::class) - ->disableOriginalConstructor() - ->getMock(); + ->willReturn($resultFactory); - $this->cartHelper = $this->getMockBuilder(\Magento\Checkout\Helper\Cart::class) - ->disableOriginalConstructor() - ->getMock(); + $this->cart = $this->createMock(Cart::class); + $this->cartHelper = $this->createMock(CartHelper::class); $this->quote = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() - ->setMethods(['getHasError']) + ->addMethods(['getHasError']) ->getMock(); - $this->optionCollection = $this->getMockBuilder( - \Magento\Wishlist\Model\ResourceModel\Item\Option\Collection::class - )->disableOriginalConstructor() - ->getMock(); + $this->optionCollection = $this->createMock(OptionCollection::class); $this->option = $this->getMockBuilder(Option::class) ->disableOriginalConstructor() ->getMock(); - $this->optionFactory = $this->getMockBuilder(OptionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->optionFactory->expects($this->once()) + /** @var OptionFactory|MockObject $optionFactory */ + $optionFactory = $this->createMock(OptionFactory::class); + $optionFactory->expects($this->once()) ->method('create') ->willReturn($this->option); - $this->item = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->getMock(); + $this->item = $this->createMock(Item::class); - $this->itemFactory = $this->getMockBuilder(ItemFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->itemFactory->expects($this->once()) + $itemFactory = $this->createMock(ItemFactory::class); + $itemFactory->expects($this->once()) ->method('create') ->willReturn($this->item); - $this->escaper = $this->getMockBuilder(Escaper::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); + $this->escaper = $this->createMock(Escaper::class); + $this->product = $this->createMock(Product::class); $this->model = new SharedCart( - $this->context, + $context, $this->cart, - $this->optionFactory, - $this->itemFactory, + $optionFactory, + $itemFactory, $this->cartHelper, $this->escaper ); @@ -358,7 +349,7 @@ public function testExecuteProductException() $this->option->expects($this->once()) ->method('getCollection') - ->willThrowException(new \Magento\Catalog\Model\Product\Exception(__('LocalizedException'))); + ->willThrowException(new Exception(__('LocalizedException'))); $this->resultRedirect->expects($this->once()) ->method('setUrl') diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php index 3489585cd17d7..840c4638614c4 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php @@ -83,7 +83,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $customerId = $context->getUserId(); diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php index cad574ef56ed2..b73afe27883dd 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php @@ -54,7 +54,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } if (false === $context->getExtensionAttributes()->getIsCustomer()) { diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlists.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlists.php new file mode 100644 index 0000000000000..ad0c73691720a --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlists.php @@ -0,0 +1,102 @@ +<?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\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist\Collection as WishlistCollection; +use Magento\Wishlist\Model\ResourceModel\Wishlist\CollectionFactory as WishlistCollectionFactory; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Fetches customer wishlist list + */ +class CustomerWishlists implements ResolverInterface +{ + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistCollectionFactory + */ + private $wishlistCollectionFactory; + + /** + * @param WishlistDataMapper $wishlistDataMapper + * @param WishlistConfig $wishlistConfig + * @param WishlistCollectionFactory $wishlistCollectionFactory + */ + public function __construct( + WishlistDataMapper $wishlistDataMapper, + WishlistConfig $wishlistConfig, + WishlistCollectionFactory $wishlistCollectionFactory + ) { + $this->wishlistDataMapper = $wishlistDataMapper; + $this->wishlistConfig = $wishlistConfig; + $this->wishlistCollectionFactory = $wishlistCollectionFactory; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); + } + + $customerId = $context->getUserId(); + + if (null === $customerId || 0 === $customerId) { + throw new GraphQlAuthorizationException( + __('The current user cannot perform operations on wishlist') + ); + } + + $currentPage = $args['currentPage'] ?? 1; + $pageSize = $args['pageSize'] ?? 20; + + /** @var WishlistCollection $collection */ + $collection = $this->wishlistCollectionFactory->create(); + $collection->filterByCustomerId($customerId); + + if ($currentPage > 0) { + $collection->setCurPage($currentPage); + } + + if ($pageSize > 0) { + $collection->setPageSize($pageSize); + } + + $wishlists = []; + + /** @var Wishlist $wishList */ + foreach ($collection->getItems() as $wishList) { + array_push($wishlists, $this->wishlistDataMapper->map($wishList)); + } + + return $wishlists; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/ProductResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/ProductResolver.php index 65c8498fc89ad..31dd33ff2cd79 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/ProductResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/ProductResolver.php @@ -7,12 +7,12 @@ namespace Magento\WishlistGraphQl\Model\Resolver; +use Magento\Catalog\Model\Product; 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 @@ -45,9 +45,9 @@ public function resolve( if (!isset($value['model'])) { throw new LocalizedException(__('Missing key "model" in Wishlist Item value data')); } - /** @var Item $wishlistItem */ - $wishlistItem = $value['model']; + /** @var Product $product */ + $product = $value['model']; - return $this->productDataProvider->getProductDataById((int)$wishlistItem->getProductId()); + return $this->productDataProvider->getProductDataById((int) $product->getId()); } } diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php index a59c5ccdb0f70..66a6c7b86ea37 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php @@ -83,7 +83,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $customerId = $context->getUserId(); diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/Type/WishlistItemType.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/Type/WishlistItemType.php new file mode 100644 index 0000000000000..ae4a6ed2b6a64 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/Type/WishlistItemType.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver\Type; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Resolving the wishlist item type + */ +class WishlistItemType implements TypeResolverInterface +{ + /** + * @var array + */ + private $supportedTypes = []; + + /** + * @param array $supportedTypes + */ + public function __construct(array $supportedTypes = []) + { + $this->supportedTypes = $supportedTypes; + } + + /** + * Resolving wishlist item type + * + * @param array $data + * + * @return string + * + * @throws LocalizedException + */ + public function resolveType(array $data): string + { + if (!$data['model'] instanceof ProductInterface) { + throw new LocalizedException(__('"model" should be a "%instance" instance', [ + 'instance' => ProductInterface::class + ])); + } + + $productTypeId = $data['model']->getTypeId(); + + 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/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php index c6ede66fc2b1b..47a408d55555b 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -83,7 +83,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $customerId = $context->getUserId(); diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistById.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistById.php new file mode 100644 index 0000000000000..1ddf91637fe90 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistById.php @@ -0,0 +1,115 @@ +<?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\Exception\GraphQlInputException; +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\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Fetches the Wishlist data by ID according to the GraphQL schema + */ +class WishlistById implements ResolverInterface +{ + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistDataMapper $wishlistDataMapper + * @param WishlistConfig $wishlistConfig + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistDataMapper $wishlistDataMapper, + WishlistConfig $wishlistConfig + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistDataMapper = $wishlistDataMapper; + $this->wishlistConfig = $wishlistConfig; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); + } + + $customerId = $context->getUserId(); + + if (null === $customerId || 0 === $customerId) { + throw new GraphQlAuthorizationException( + __('The current user cannot perform operations on wishlist') + ); + } + + $wishlist = $this->getWishlist((int) $args['id'], $customerId); + + if (null === $wishlist->getId() || (int) $wishlist->getCustomerId() !== $customerId) { + return []; + } + + return $this->wishlistDataMapper->map($wishlist); + } + + /** + * Get wishlist + * + * @param int $wishlistId + * @param int $customerId + * + * @return Wishlist + */ + private function getWishlist(int $wishlistId, int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } else { + $this->wishlistResource->load($wishlist, $customerId, 'customer_id'); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php new file mode 100644 index 0000000000000..77ff483a60bd2 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItems.php @@ -0,0 +1,98 @@ +<?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 WishlistItems 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(), + 'quantity' => $wishlistItem->getData('qty'), + 'description' => $wishlistItem->getDescription(), + 'added_at' => $wishlistItem->getAddedAt(), + 'model' => $wishlistItem->getProduct(), + 'itemModel' => $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/WishlistItemsResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItemsResolver.php index dfbbf6543f66f..36a03da2b79a9 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItemsResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistItemsResolver.php @@ -70,7 +70,7 @@ public function resolve( 'qty' => $wishlistItem->getData('qty'), 'description' => $wishlistItem->getDescription(), 'added_at' => $wishlistItem->getAddedAt(), - 'model' => $wishlistItem, + 'model' => $wishlistItem->getProduct(), ]; } return $data; diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php index 09c0a8a935a6c..f31b403a514fb 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -63,7 +63,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $customerId = $context->getUserId(); diff --git a/app/code/Magento/WishlistGraphQl/composer.json b/app/code/Magento/WishlistGraphQl/composer.json index 7a3fca599a4b3..58bc738bd24d6 100644 --- a/app/code/Magento/WishlistGraphQl/composer.json +++ b/app/code/Magento/WishlistGraphQl/composer.json @@ -5,6 +5,7 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/module-catalog": "*", "magento/module-catalog-graph-ql": "*", "magento/module-wishlist": "*", "magento/module-store": "*" diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index 430e77cc45e96..69bc45462d4c8 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -6,7 +6,12 @@ type Query { } type Customer { - wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "Contains the contents of a customer's wish lists") @cache(cacheable: false) + wishlists( + 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.") + ): [Wishlist!]! @doc(description: "An array of wishlists. In Magento Open Source, customers are limited to one wish list. The number of wish lists is configurable for Magento Commerce") @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlists") + wishlist: Wishlist! @deprecated(reason: "Use `Customer.wishlists` or `Customer.wishlist_v2`") @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "Contains a customer's wish lists") @cache(cacheable: false) + wishlist_v2(id: ID!): Wishlist @doc(description: "Retrieve the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistById") } type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be used instead") { @@ -19,12 +24,22 @@ type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be use type Wishlist { id: ID @doc(description: "Wishlist unique identifier") - 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"), - sharing_code: String @doc(description: "An encrypted code that Magento uses to link to the wish list"), + items: [WishlistItem] @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItemsResolver") @deprecated(reason: "Use field `items_v2` from type `Wishlist` instead") + items_v2: [WishlistItemInterface] @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\WishlistItems") @doc(description: "An array of items in the customer's wish list") + items_count: Int @doc(description: "The number of items in the wish list") + sharing_code: String @doc(description: "An encrypted code that Magento uses to link to the wish list") updated_at: String @doc(description: "The time of the last modification to the wish list") } +interface WishlistItemInterface @typeResolver(class: "Magento\\WishlistGraphQl\\Model\\Resolver\\Type\\WishlistItemType") { + id: ID! @doc(description: "The ID of the wish list item") + quantity: Float! @doc(description: "The quantity of this wish list item") + description: String @doc(description: "The description of the item") + added_at: String! @doc(description: "The date and time the item was added to the wish list") + product: ProductInterface @doc(description: "Product details of the wish list item") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\ProductResolver") + customizable_options: [SelectedCustomizableOption] @doc(description: "Custom options selected for the wish list item") +} + type WishlistItem { id: Int @doc(description: "The wish list item ID") qty: Float @doc(description: "The quantity of this wish list item"), diff --git a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less index fa158589feb96..654236e143a29 100644 --- a/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less +++ b/app/design/adminhtml/Magento/backend/Magento_ConfigurableProduct/web/css/source/module/components/_currency-addon.less @@ -18,15 +18,10 @@ // _____________________________________________ .currency-addon { + .lib-vendor-prefix-display(inline-flex); border: 1px solid rgb(173,173,173); - position: relative; - display: -webkit-inline-flex; - display: -ms-inline-flexbox; - display: inline-flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; flex-flow: row nowrap; + position: relative; width: 100%; .admin__control-text { diff --git a/app/design/adminhtml/Magento/backend/web/css/source/components/_image-uploader.less b/app/design/adminhtml/Magento/backend/web/css/source/components/_image-uploader.less index f187697281252..a9172d5164c38 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/components/_image-uploader.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/components/_image-uploader.less @@ -9,8 +9,8 @@ .image-uploader { .image-upload-requirements { - margin-top: 8px; font-size: .9em; + margin-top: 8px; } .image-placeholder { @@ -19,12 +19,12 @@ } .image-uploader-spinner { - width: 50%; - height: 50%; background-size: auto; + height: 50%; margin: 0; - transform: translate(50%, 50%); position: absolute; + transform: translate(50%, 50%); + width: 50%; } .image-uploader-preview { @@ -33,7 +33,10 @@ .image-uploader-preview-link, .image-uploader-preview-link .preview-image { + display: block; height: inherit; + margin-left: auto; + margin-right: auto; } } 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 5bdaa4c3c35a3..690b89f42b419 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_extends.less +++ b/app/design/frontend/Magento/blank/web/css/source/_extends.less @@ -1110,7 +1110,7 @@ .abs-shopping-cart-items { .action { &.continue { - border-radius: 3px; + border-radius: @button__border-radius; font-weight: @font-weight__bold; .lib-link-as-button(); .lib-button( diff --git a/app/design/frontend/Magento/blank/web/css/source/_icons.less b/app/design/frontend/Magento/blank/web/css/source/_icons.less index 7d1ceaca73c72..5cd7795aa506c 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_icons.less +++ b/app/design/frontend/Magento/blank/web/css/source/_icons.less @@ -8,6 +8,7 @@ @family-name: @icons__font-name, @font-path: @icons__font-path, @font-weight: normal, - @font-style: normal + @font-style: normal, + @font-display: block ); } 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 fad906a089400..f9cca1ca16a18 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_navigation.less +++ b/app/design/frontend/Magento/blank/web/css/source/_navigation.less @@ -28,10 +28,10 @@ .nav-toggle { .lib-icon-font( - @icon-menu, - @_icon-font-size: 28px, - @_icon-font-color: @header-icons-color, - @_icon-font-color-hover: @header-icons-color-hover + @icon-menu, + @_icon-font-size: 28px, + @_icon-font-color: @header-icons-color, + @_icon-font-color-hover: @header-icons-color-hover ); .lib-icon-text-hide(); cursor: pointer; @@ -54,13 +54,13 @@ .parent { .level-top { - position: relative; .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: 42px, - @_icon-font-position: after, - @_icon-font-display: block + @_icon-font-content: @icon-down, + @_icon-font-size: 42px, + @_icon-font-position: after, + @_icon-font-display: block ); + position: relative; &:after { position: absolute; @@ -70,8 +70,8 @@ &.ui-state-active { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } } @@ -82,12 +82,10 @@ -webkit-overflow-scrolling: touch; .lib-css(transition, left .3s, 1); height: 100%; - left: -80%; left: calc(~'-1 * (100% - @{active-nav-indent})'); overflow: auto; position: fixed; top: 0; - width: 80%; width: calc(~'100% - @{active-nav-indent}'); .switcher { @@ -109,13 +107,13 @@ .switcher-trigger { strong { - position: relative; .lib-icon-font( - @_icon-font-content: @icon-down, - @_icon-font-size: 42px, - @_icon-font-position: after, - @_icon-font-display: block + @_icon-font-content: @icon-down, + @_icon-font-size: 42px, + @_icon-font-position: after, + @_icon-font-display: block ); + position: relative; &:after { position: absolute; @@ -126,16 +124,18 @@ &.active strong { .lib-icon-font-symbol( - @_icon-font-content: @icon-up, - @_icon-font-position: after + @_icon-font-content: @icon-up, + @_icon-font-position: after ); } } + .switcher-dropdown { .lib-list-reset-styles(); display: none; padding: @indent__s 0; } + .switcher-options { &.active { .switcher-dropdown { @@ -143,6 +143,7 @@ } } } + .header.links { .lib-list-reset-styles(); border-bottom: 1px solid @color-gray82; @@ -200,13 +201,11 @@ .nav-open { .page-wrapper { - left: 80%; left: calc(~'100% - @{active-nav-indent}'); } .nav-sections { @_shadow: 0 0 5px 0 rgba(50, 50, 50, .75); - .lib-css(box-shadow, @_shadow, 1); left: 0; z-index: 99; @@ -293,10 +292,6 @@ display: none; } - .nav-sections-item-content { - display: block !important; - } - .nav-sections-item-content > * { display: none; } diff --git a/app/design/frontend/Magento/blank/web/css/source/_typography.less b/app/design/frontend/Magento/blank/web/css/source/_typography.less index 6807c0f692af8..02ccd90d4655d 100644 --- a/app/design/frontend/Magento/blank/web/css/source/_typography.less +++ b/app/design/frontend/Magento/blank/web/css/source/_typography.less @@ -9,7 +9,7 @@ & when (@media-common = true) { .lib-font-face( - @family-name: @font-family-name__base, + @family-name: 'Open Sans', @font-path: '@{baseDir}fonts/opensans/light/opensans-300', @font-weight: 300, @font-style: normal, @@ -17,7 +17,7 @@ ); .lib-font-face( - @family-name: @font-family-name__base, + @family-name: 'Open Sans', @font-path: '@{baseDir}fonts/opensans/regular/opensans-400', @font-weight: 400, @font-style: normal, @@ -25,7 +25,7 @@ ); .lib-font-face( - @family-name: @font-family-name__base, + @family-name: 'Open Sans', @font-path: '@{baseDir}fonts/opensans/semibold/opensans-600', @font-weight: 600, @font-style: normal, @@ -33,7 +33,7 @@ ); .lib-font-face( - @family-name: @font-family-name__base, + @family-name: 'Open Sans', @font-path: '@{baseDir}fonts/opensans/bold/opensans-700', @font-weight: 700, @font-style: normal, diff --git a/app/etc/di.xml b/app/etc/di.xml index fed2e336046f9..585c88f68ff6f 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -185,6 +185,7 @@ <preference for="Magento\Framework\Setup\Declaration\Schema\Db\DbSchemaWriterInterface" type="Magento\Framework\Setup\Declaration\Schema\Db\MySQL\DbSchemaWriter" /> <preference for="Magento\Framework\Setup\Declaration\Schema\SchemaConfigInterface" type="Magento\Framework\Setup\Declaration\Schema\SchemaConfig" /> <preference for="Magento\Framework\Setup\Declaration\Schema\DataSavior\DumpAccessorInterface" type="Magento\Framework\Setup\Declaration\Schema\FileSystem\Csv" /> + <preference for="Magento\Framework\MessageQueue\ConfigInterface" type="Magento\Framework\MessageQueue\Config\Proxy" /> <preference for="Magento\Framework\MessageQueue\PublisherInterface" type="Magento\Framework\MessageQueue\PublisherPool" /> <preference for="Magento\Framework\MessageQueue\BulkPublisherInterface" type="Magento\Framework\MessageQueue\Bulk\PublisherPool" /> <preference for="Magento\Framework\MessageQueue\MessageIdGeneratorInterface" type="Magento\Framework\MessageQueue\MessageIdGenerator" /> diff --git a/composer.json b/composer.json index 25be12b5bb72f..57fbfaaa35c2b 100644 --- a/composer.json +++ b/composer.json @@ -215,12 +215,15 @@ "magento/module-media-content-synchronization-api": "*", "magento/module-media-content-synchronization-catalog": "*", "magento/module-media-content-synchronization-cms": "*", + "magento/module-media-gallery-synchronization-metadata": "*", "magento/module-media-gallery-metadata": "*", "magento/module-media-gallery-metadata-api": "*", "magento/module-media-gallery-catalog-ui": "*", "magento/module-media-gallery-cms-ui": "*", "magento/module-media-gallery-catalog-integration": "*", "magento/module-media-gallery-catalog": "*", + "magento/module-media-gallery-renditions": "*", + "magento/module-media-gallery-renditions-api": "*", "magento/module-media-storage": "*", "magento/module-message-queue": "*", "magento/module-msrp": "*", diff --git a/composer.lock b/composer.lock index c2eed9d87cc00..8a5d82536cee4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0b51badfd1978bb34febd90226af9e27", + "content-hash": "a03edc1c8ee05f82886eebd6ed288df8", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -206,6 +206,16 @@ "ssl", "tls" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], "time": "2020-04-08T08:27:21+00:00" }, { @@ -1346,12 +1356,6 @@ "BSD-3-Clause" ], "description": "Replace zendframework and zfcampus packages with their Laminas Project equivalents.", - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-05-20T13:45:39+00:00" }, { @@ -3319,12 +3323,6 @@ "laminas", "zf" ], - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-05-20T16:45:56+00:00" }, { @@ -3564,16 +3562,6 @@ "logging", "psr-3" ], - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", - "type": "tidelift" - } - ], "time": "2020-05-22T07:31:27+00:00" }, { @@ -4366,16 +4354,6 @@ "parser", "validator" ], - "funding": [ - { - "url": "https://github.com/Seldaek", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", - "type": "tidelift" - } - ], "time": "2020-04-30T19:05:18+00:00" }, { @@ -7337,6 +7315,555 @@ ], "time": "2020-06-27T23:57:46+00:00" }, + { + "name": "hoa/consistency", + "version": "1.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Consistency.git", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", + "shasum": "" + }, + "require": { + "hoa/exception": "~1.0", + "php": ">=5.5.0" + }, + "require-dev": { + "hoa/stream": "~1.0", + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Consistency\\": "." + }, + "files": [ + "Prelude.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Consistency library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autoloader", + "callable", + "consistency", + "entity", + "flex", + "keyword", + "library" + ], + "time": "2017-05-02T12:18:12+00:00" + }, + { + "name": "hoa/console", + "version": "3.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Console.git", + "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Console/zipball/e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/file": "~1.0", + "hoa/protocol": "~1.0", + "hoa/stream": "~1.0", + "hoa/ustring": "~4.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-pcntl": "To enable hoa://Event/Console/Window:resize.", + "hoa/dispatcher": "To use the console kit.", + "hoa/router": "To use the console kit." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Console\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Console library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autocompletion", + "chrome", + "cli", + "console", + "cursor", + "getoption", + "library", + "option", + "parser", + "processus", + "readline", + "terminfo", + "tput", + "window" + ], + "time": "2017-05-02T12:26:19+00:00" + }, + { + "name": "hoa/event", + "version": "1.17.01.13", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Event.git", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Event\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Event library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "event", + "library", + "listener", + "observer" + ], + "time": "2017-01-13T15:30:50+00:00" + }, + { + "name": "hoa/exception", + "version": "1.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Exception.git", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Exception\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Exception library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "exception", + "library" + ], + "time": "2017-01-16T07:53:27+00:00" + }, + { + "name": "hoa/file", + "version": "1.17.07.11", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/File.git", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/iterator": "~2.0", + "hoa/stream": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\File\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\File library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Socket", + "directory", + "file", + "finder", + "library", + "link", + "temporary" + ], + "time": "2017-07-11T07:42:15+00:00" + }, + { + "name": "hoa/iterator", + "version": "2.17.01.10", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Iterator.git", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Iterator\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Iterator library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "iterator", + "library" + ], + "time": "2017-01-10T10:34:47+00:00" + }, + { + "name": "hoa/protocol", + "version": "1.17.01.14", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Protocol.git", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Protocol\\": "." + }, + "files": [ + "Wrapper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Protocol library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "protocol", + "resource", + "stream", + "wrapper" + ], + "time": "2017-01-14T12:26:10+00:00" + }, + { + "name": "hoa/stream", + "version": "1.17.02.21", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Stream.git", + "reference": "3293cfffca2de10525df51436adf88a559151d82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", + "reference": "3293cfffca2de10525df51436adf88a559151d82", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/protocol": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Stream\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Stream library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Context", + "bucket", + "composite", + "filter", + "in", + "library", + "out", + "protocol", + "stream", + "wrapper" + ], + "time": "2017-02-21T16:01:06+00:00" + }, + { + "name": "hoa/ustring", + "version": "4.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Ustring.git", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", + "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Ustring\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Ustring library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "search", + "string", + "unicode" + ], + "time": "2017-01-16T07:08:25+00:00" + }, { "name": "jms/metadata", "version": "1.7.0", @@ -7593,12 +8120,6 @@ "sftp", "storage" ], - "funding": [ - { - "url": "https://offset.earth/frankdejonge", - "type": "other" - } - ], "time": "2020-05-18T15:13:39+00:00" }, { @@ -7709,16 +8230,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699" + "reference": "8a106ea029f222f4354854636861273c7577bee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8d98efa7434a30ab9e82ef128c430ef8e3a50699", - "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8a106ea029f222f4354854636861273c7577bee9", + "reference": "8a106ea029f222f4354854636861273c7577bee9", "shasum": "" }, "require": { @@ -7736,6 +8257,7 @@ "ext-intl": "*", "ext-json": "*", "ext-openssl": "*", + "hoa/console": "~3.0", "monolog/monolog": "^1.17", "mustache/mustache": "~2.5", "php": "^7.3", @@ -7795,7 +8317,7 @@ "magento", "testing" ], - "time": "2020-07-09T21:26:19+00:00" + "time": "2020-08-19T19:57:27+00:00" }, { "name": "mikey179/vfsstream", @@ -8817,20 +9339,6 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" - } - ], "time": "2020-05-05T12:55:44+00:00" }, { @@ -9120,12 +9628,6 @@ "keywords": [ "timer" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-20T06:00:37+00:00" }, { @@ -9181,6 +9683,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-06-27T06:36:25+00:00" }, { @@ -9269,16 +9772,6 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://phpunit.de/donate.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-05-22T13:54:05+00:00" }, { @@ -9786,6 +10279,7 @@ ], "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.", "homepage": "https://github.com/sebastianbergmann/finder-facade", + "abandoned": true, "time": "2020-02-08T06:07:58+00:00" }, { diff --git a/dev/tests/acceptance/staticRuleset.json b/dev/tests/acceptance/staticRuleset.json index 74fe3469e353b..82cc9dfe74152 100644 --- a/dev/tests/acceptance/staticRuleset.json +++ b/dev/tests/acceptance/staticRuleset.json @@ -2,6 +2,7 @@ "tests": [ "actionGroupArguments", "deprecatedEntityUsage", - "annotations" + "annotations", + "pauseActionUsage" ] } 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 1731a974aaed3..71ff93875f2c1 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 @@ -11,6 +11,9 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; +/** + * Resolver for Item + */ class Item implements ResolverInterface { /** diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/TestUnion.php b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/TestUnion.php new file mode 100644 index 0000000000000..592b0caaa88a3 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/TestUnion.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleGraphQlQuery\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver for Union type TestUnion + */ +class TestUnion implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + return [ + 'custom_name1' => 'custom_name1_value', + 'custom_name2' => 'custom_name2_value', + ]; + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/UnionTypeResolver.php b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/UnionTypeResolver.php new file mode 100644 index 0000000000000..40cbdadb8a948 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/Model/Resolver/UnionTypeResolver.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleGraphQlQuery\Model\Resolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Type Resolver for union + */ +class UnionTypeResolver implements TypeResolverInterface +{ + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!empty($data)) { + return 'TypeCustom1'; + } + return ''; + } +} diff --git a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls index 7eb175a88e322..1a5796e07b08b 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls +++ b/dev/tests/api-functional/_files/Magento/TestModuleGraphQlQuery/etc/schema.graphqls @@ -3,6 +3,7 @@ type Query { testItem(id: Int!) : Item @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\Item") + testUnion: TestUnion @resolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\TestUnion") } type Mutation { @@ -18,3 +19,14 @@ type MutationItem { item_id: Int name: String } + +union TestUnion @doc(description: "some kind of union") @typeResolver(class: "Magento\\TestModuleGraphQlQuery\\Model\\Resolver\\UnionTypeResolver") = + TypeCustom1 | TypeCustom2 + +type TypeCustom1 { + custom_name1: String +} + +type TypeCustom2 { + custom_name2: String +} diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php index d99b056ca359e..59aa2e7c719bf 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/Annotation/ApiDataFixture.php @@ -32,15 +32,20 @@ public function startTest(TestCase $test) { Bootstrap::getInstance()->reinitialize(); /** Apply method level fixtures if thy are available, apply class level fixtures otherwise */ - $this->_applyFixtures($this->_getFixtures($test, 'method') ?: $this->_getFixtures($test, 'class')); + $this->_applyFixtures( + $this->_getFixtures($test, 'method') ?: $this->_getFixtures($test, 'class'), + $test + ); } /** * Handler for 'endTest' event + * + * @param TestCase $test */ - public function endTest() + public function endTest(TestCase $test) { - $this->_revertFixtures(); + $this->_revertFixtures($test); $objectManager = Bootstrap::getObjectManager(); $objectManager->get(AttributeMetadataCache::class)->clean(); } 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 5af6413840c27..2fe93c02e7adb 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 @@ -213,7 +213,7 @@ private function processResponseHeaders(string $headers): array $headerLines = preg_split('/((\r?\n)|(\r\n?))/', $headers); foreach ($headerLines as $headerLine) { - $headerParts = preg_split('/:/', $headerLine); + $headerParts = preg_split('/: /', $headerLine, 2); if (count($headerParts) == 2) { $headersArray[trim($headerParts[0])] = trim($headerParts[1]); } elseif (preg_match('/HTTP\/[\.0-9]+/', $headerLine)) { 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 7f67c8c9ca8df..3de18a932f2cd 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php @@ -184,61 +184,4 @@ protected function assertResponseFields($actualResponse, $assertionMap) ); } } - - /** - * Compare arrays recursively regardless of nesting. - * - * Can compare arrays that have both one level and n-level nesting. - * ``` - * [ - * 'products' => [ - * 'items' => [ - * [ - * 'sku' => 'bundle-product', - * 'type_id' => 'bundle', - * 'items' => [ - * [ - * 'title' => 'Bundle Product Items', - * 'sku' => 'bundle-product', - * 'options' => [ - * [ - * 'price' => 2.75, - * 'label' => 'Simple Product', - * 'product' => [ - * 'name' => 'Simple Product', - * 'sku' => 'simple', - * ] - * ] - * ] - * ] - * ]; - * ``` - * - * @param array $expected - * @param array $actual - * @return array - */ - public function compareArraysRecursively(array $expected, array $actual): array - { - $diffResult = []; - - foreach ($expected as $key => $value) { - if (array_key_exists($key, $actual)) { - if (is_array($value)) { - $recursiveDiff = $this->compareArraysRecursively($value, $actual[$key]); - if (!empty($recursiveDiff)) { - $diffResult[$key] = $recursiveDiff; - } - } else { - if (!in_array($value, $actual, true)) { - $diffResult[$key] = $value; - } - } - } else { - $diffResult[$key] = $value; - } - } - - return $diffResult; - } } diff --git a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php index 7a4f472c69513..538c0b0ee5fac 100644 --- a/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Bundle/Api/ProductServiceTest.php @@ -225,6 +225,7 @@ public function testUpdateBundleAddSelection() public function testUpdateBundleAddAndDeleteOption() { $bundleProduct = $this->createDynamicBundleProduct(); + $linkedProductPrice = 20; $bundleProductOptions = $this->getBundleProductOptions($bundleProduct); @@ -238,7 +239,7 @@ public function testUpdateBundleAddAndDeleteOption() [ 'sku' => 'simple2', 'qty' => 2, - "price" => 20, + "price" => $linkedProductPrice, "price_type" => 1, "is_default" => false, ], @@ -256,6 +257,7 @@ public function testUpdateBundleAddAndDeleteOption() $this->assertFalse(isset($bundleOptions[1])); $this->assertEquals('simple2', $bundleOptions[0]['product_links'][0]['sku']); $this->assertEquals(2, $bundleOptions[0]['product_links'][0]['qty']); + $this->assertEquals($linkedProductPrice, $bundleOptions[0]['product_links'][0]['price']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php index 21b93645fd15a..461ab6c989104 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -43,6 +43,11 @@ class CategoryRepositoryTest extends WebapiAbstract */ private $adminTokens; + /** + * @var string[] + */ + private $createdCategories; + /** * @inheritDoc */ @@ -132,8 +137,7 @@ public function testCreate() sprintf('"%s" field value is invalid', $fieldName) ); } - // delete category to clean up auto-generated url rewrites - $this->deleteCategory($result['id']); + $this->createdCategories = [$result['id']]; } /** @@ -214,8 +218,7 @@ public function testUpdate() $this->assertFalse((bool)$category->getIsActive(), 'Category "is_active" must equal to false'); $this->assertEquals("Update Category Test", $category->getName()); $this->assertEquals("Update Category Description Test", $category->getDescription()); - // delete category to clean up auto-generated url rewrites - $this->deleteCategory($categoryId); + $this->createdCategories = [$categoryId]; } /** @@ -243,8 +246,7 @@ public function testUpdateWithDefaultSortByAttribute() $this->assertTrue((bool)$category->getIsActive(), 'Category "is_active" must equal to true'); $this->assertEquals("Update Category Test With default_sort_by Attribute", $category->getName()); $this->assertEquals("name", $category->getDefaultSortBy()); - // delete category to clean up auto-generated url rewrites - $this->deleteCategory($categoryId); + $this->createdCategories = [$categoryId]; } protected function getSimpleCategoryData($categoryData = []) @@ -476,5 +478,23 @@ public function testSaveDesign(): void } //We don't have permissions to do that. $this->assertEquals('Not allowed to edit the category\'s design attributes', $exceptionMessage); + $this->createdCategories = [$result['id']]; + } + + /** + * @inheritDoc + * + * @return void + */ + protected function tearDown(): void + { + if (!empty($this->createdCategories)) { + // delete category to clean up auto-generated url rewrites + foreach ($this->createdCategories as $categoryId) { + $this->deleteCategory($categoryId); + } + } + + parent::tearDown(); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMultipleOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMultipleOptionsTest.php index 3409b5e3af1af..77c4d5b84e72e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMultipleOptionsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductMultipleOptionsTest.php @@ -7,6 +7,8 @@ namespace Magento\GraphQl\Bundle; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\CompareArraysRecursively; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -14,6 +16,20 @@ */ class BundleProductMultipleOptionsTest extends GraphQlAbstract { + /** + * @var CompareArraysRecursively + */ + private $compareArraysRecursively; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->compareArraysRecursively = $objectManager->create(CompareArraysRecursively::class); + } + /** * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options.php * @param array $bundleProductDataProvider @@ -85,7 +101,7 @@ private function assertBundleProduct(array $response, array $bundleProductDataPr $productItems = $response['products']['items']; foreach ($bundleProductDataProvider as $key => $data) { - $diff = $this->compareArraysRecursively($data, $productItems[$key]); + $diff = $this->compareArraysRecursively->execute($data, $productItems[$key]); self::assertEquals([], $diff, "Actual response doesn't equal to expected data"); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php index 25c808a549e80..b8f59b34fae0c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php @@ -76,7 +76,7 @@ public function testCorsHeadersWhenCorsIsEnabled(): void $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); - $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'http://magento.local'); $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); $this->reinitConfig->reinit(); @@ -85,7 +85,7 @@ public function testCorsHeadersWhenCorsIsEnabled(): void self::assertEquals('Origin', $headers['Access-Control-Allow-Headers']); self::assertEquals('1', $headers['Access-Control-Allow-Credentials']); self::assertEquals('GET,POST', $headers['Access-Control-Allow-Methods']); - self::assertEquals('magento.local', $headers['Access-Control-Allow-Origin']); + self::assertEquals('http://magento.local', $headers['Access-Control-Allow-Origin']); self::assertEquals('86400', $headers['Access-Control-Max-Age']); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php index c2f94128ef8ec..cb210b180682c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/RelatedProduct/GetRelatedProductsTest.php @@ -10,7 +10,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Get related products test + * Test coverage for get related products */ class GetRelatedProductsTest extends GraphQlAbstract { @@ -49,6 +49,40 @@ public function testQueryRelatedProducts() self::assertRelatedProducts($relatedProducts); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_related_disabled.php + */ + public function testQueryDisableRelatedProduct() + { + $productSku = 'simple_with_cross'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + related_products + { + sku + name + url_key + created_at + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertCount(1, $response['products']['items']); + self::assertArrayHasKey(0, $response['products']['items']); + self::assertArrayHasKey('related_products', $response['products']['items'][0]); + $relatedProducts = $response['products']['items'][0]['related_products']; + self::assertCount(0, $relatedProducts); + } + /** * @magentoApiDataFixture Magento/Catalog/_files/products_crosssell.php */ 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 2db06e383758f..0bbdf5a4c9803 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/TestModule/GraphQlQueryTest.php @@ -10,7 +10,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Class GraphQlQueryTest + * Test for basic GraphQl features */ class GraphQlQueryTest extends GraphQlAbstract { @@ -100,4 +100,29 @@ public function testQueryViaGetRequestWithVariablesReturnsResults() $this->assertArrayHasKey('testItem', $response); } + + public function testQueryTestUnionResults() + { + $query = <<<QUERY +{ + testUnion { + __typename + ... on TypeCustom1 { + custom_name1 + } + ... on TypeCustom2 { + custom_name2 + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('testUnion', $response); + $testUnion = $response['testUnion']; + $this->assertArrayHasKey('custom_name1', $testUnion); + $this->assertEquals('custom_name1_value', $testUnion['custom_name1']); + $this->assertArrayNotHasKey('custom_name2', $testUnion); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php index a81ec701b22a8..b97cd379e4384 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php @@ -16,6 +16,7 @@ use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Ui\Component\Form\Element\Select; use Magento\Wishlist\Model\Item; use Magento\Wishlist\Model\WishlistFactory; @@ -74,7 +75,7 @@ public function testAddBundleProductWithOptions(): void $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); $optionId = $option->getId(); $selectionId = $selection->getSelectionId(); - $bundleOptions = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, $optionQty); + $bundleOptions = $this->generateBundleOptionUid((int) $optionId, (int) $selectionId, $optionQty); $query = $this->getQuery($sku, $qty, $bundleOptions); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); @@ -88,9 +89,13 @@ public function testAddBundleProductWithOptions(): void $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); - $this->assertEquals($item->getData('qty'), $response['items'][0]['qty']); - $this->assertEquals($item->getDescription(), $response['items'][0]['description']); - $this->assertEquals($item->getAddedAt(), $response['items'][0]['added_at']); + $this->assertEquals($item->getData('qty'), $response['items_v2'][0]['quantity']); + $this->assertEquals($item->getDescription(), $response['items_v2'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); + $this->assertNotEmpty($response['items_v2'][0]['bundle_options']); + $bundleOptions = $response['items_v2'][0]['bundle_options']; + $this->assertEquals('Bundle Product Items', $bundleOptions[0]['label']); + $this->assertEquals(Select::NAME, $bundleOptions[0]['type']); } /** @@ -149,11 +154,24 @@ private function getQuery( sharing_code items_count updated_at - items { + items_v2 { id description - qty + quantity added_at + ... on BundleWishlistItem { + bundle_options { + id + label + type + values { + id + label + quantity + price + } + } + } } } } @@ -169,7 +187,7 @@ private function getQuery( * * @return string */ - private function generateBundleOptionIdV2(int $optionId, int $selectionId, int $quantity): string + private function generateBundleOptionUid(int $optionId, int $selectionId, int $quantity): string { return base64_encode("bundle/$optionId/$selectionId/$quantity"); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php index d8d44541f899d..cffc5eb6f93c1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php @@ -48,7 +48,7 @@ protected function setUp(): void * * @throws Exception */ - public function testAddDownloadableProductWithOptions(): void + public function testAddConfigurableProductWithOptions(): void { $product = $this->getConfigurableProductInfo(); $customerId = 1; @@ -57,7 +57,7 @@ public function testAddDownloadableProductWithOptions(): void $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; $childSku = $product['variants'][0]['product']['sku']; $parentSku = $product['sku']; - $selectedConfigurableOptionsQuery = $this->generateSuperAttributesIdV2Query($attributeId, $valueIndex); + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUidQuery($attributeId, $valueIndex); $query = $this->getQuery($parentSku, $childSku, $qty, $selectedConfigurableOptionsQuery); @@ -66,16 +66,19 @@ public function testAddDownloadableProductWithOptions(): void /** @var Item $wishlistItem */ $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); - self::assertArrayHasKey('addProductsToWishlist', $response); - self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $this->assertArrayHasKey('addProductsToWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); $wishlistResponse = $response['addProductsToWishlist']['wishlist']; - self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); - self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); - self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); - self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']); - self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']); - self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']); - self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']); + $this->assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); + $this->assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); + $this->assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); + $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2'][0]['id']); + $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2'][0]['quantity']); + $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2'][0]['description']); + $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2'][0]['added_at']); + $this->assertNotEmpty($wishlistResponse['items_v2'][0]['configurable_options']); + $configurableOptions = $wishlistResponse['items_v2'][0]['configurable_options']; + $this->assertEquals('Test Configurable', $configurableOptions[0]['option_label']); } /** @@ -135,11 +138,20 @@ private function getQuery( sharing_code items_count updated_at - items { + items_v2 { id description - qty + quantity added_at + ... on ConfigurableWishlistItem { + child_sku + configurable_options { + id + option_label + value_id + value_label + } + } } } } @@ -148,14 +160,14 @@ private function getQuery( } /** - * Generates Id_v2 for super configurable product super attributes + * Generates uid for super configurable product super attributes * * @param int $attributeId * @param int $valueIndex * * @return string */ - private function generateSuperAttributesIdV2Query(int $attributeId, int $valueIndex): string + private function generateSuperAttributesUidQuery(int $attributeId, int $valueIndex): string { return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php index 489a960056f1b..0de45fb21b20b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -37,9 +37,9 @@ class AddDownloadableProductToWishlistTest extends GraphQlAbstract private $wishlistFactory; /** - * @var GetCustomOptionsWithIDV2ForQueryBySku + * @var GetCustomOptionsWithUidForQueryBySku */ - private $getCustomOptionsWithIDV2ForQueryBySku; + private $getCustomOptionsWithUidForQueryBySku; /** * Set Up @@ -49,69 +49,75 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); $this->wishlistFactory = $this->objectManager->get(WishlistFactory::class); - $this->getCustomOptionsWithIDV2ForQueryBySku = - $this->objectManager->get(GetCustomOptionsWithIDV2ForQueryBySku::class); + $this->getCustomOptionsWithUidForQueryBySku = + $this->objectManager->get(GetCustomOptionsWithUidForQueryBySku::class); } /** - * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoConfigFixture default_store wishlist/general/active 0 * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php */ - public function testAddDownloadableProductWithOptions(): void + public function testAddDownloadableProductOnDisabledWishlist(): void { - $customerId = 1; - $sku = 'downloadable-product-with-purchased-separately-links'; $qty = 2; + $sku = 'downloadable-product-with-purchased-separately-links'; $links = $this->getProductsLinks($sku); $linkId = key($links); - $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $itemOptions = $this->getCustomOptionsWithUidForQueryBySku->execute($sku); $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); - $productOptionsQuery = preg_replace( + $productOptionsQuery = trim(preg_replace( '/"([^"]+)"\s*:\s*/', '$1:', json_encode($itemOptions) - ); - $query = $this->getQuery($qty, $sku, trim($productOptionsQuery, '{}')); - $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - $wishlist = $this->wishlistFactory->create(); - $wishlist->loadByCustomerId($customerId, true); - /** @var Item $wishlistItem */ - $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); - - self::assertArrayHasKey('addProductsToWishlist', $response); - self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']); - $wishlistResponse = $response['addProductsToWishlist']['wishlist']; - self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); - self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); - self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); - self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']); - self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']); - self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']); - self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']); + ), '{}'); + $query = $this->getQuery($qty, $sku, $productOptionsQuery); + $this->expectExceptionMessage('The wishlist configuration is currently disabled.'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } /** - * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php */ - public function testAddDownloadableProductOnDisabledWishlist(): void + public function testAddDownloadableProductWithOptions(): void { - $qty = 2; + $customerId = 1; $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 2; $links = $this->getProductsLinks($sku); $linkId = key($links); - $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $itemOptions = $this->getCustomOptionsWithUidForQueryBySku->execute($sku); $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); - $productOptionsQuery = trim(preg_replace( + $productOptionsQuery = preg_replace( '/"([^"]+)"\s*:\s*/', '$1:', json_encode($itemOptions) - ), '{}'); - $query = $this->getQuery($qty, $sku, $productOptionsQuery); - $this->expectExceptionMessage('The wishlist is not currently available.'); - $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + ); + $query = $this->getQuery($qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create(); + $wishlist->loadByCustomerId($customerId, true); + /** @var Item $wishlistItem */ + $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); + + $this->assertArrayHasKey('addProductsToWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $wishlistResponse = $response['addProductsToWishlist']['wishlist']; + $this->assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); + $this->assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); + $this->assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); + $this->assertEquals($wishlistItem->getId(), $wishlistResponse['items_v2'][0]['id']); + $this->assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items_v2'][0]['quantity']); + $this->assertEquals($wishlistItem->getDescription(), $wishlistResponse['items_v2'][0]['description']); + $this->assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items_v2'][0]['added_at']); + $this->assertNotEmpty($wishlistResponse['items_v2'][0]['links_v2']); + $wishlistItemLinks = $wishlistResponse['items_v2'][0]['links_v2']; + $this->assertEquals('Downloadable Product Link 1', $wishlistItemLinks[0]['title']); + $this->assertNotEmpty($wishlistResponse['items_v2'][0]['samples']); + $wishlistItemSamples = $wishlistResponse['items_v2'][0]['samples']; + $this->assertEquals('Downloadable Product Sample', $wishlistItemSamples[0]['title']); } /** @@ -190,11 +196,23 @@ private function getQuery( sharing_code items_count updated_at - items { + items_v2 { id description - qty + quantity added_at + ... on DownloadableWishlistItem { + links_v2 { + id + title + sample_url + } + samples { + id + title + sample_url + } + } } } } @@ -203,7 +221,7 @@ private function getQuery( } /** - * Generates Id_v2 for downloadable links + * Generates uid for downloadable links * * @param int $linkId * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php index 0a8e1757a2ce2..04095c1679d2f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -131,7 +131,7 @@ public function testGuestCannotGetWishlist() public function testCustomerCannotGetWishlistWhenDisabled() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The wishlist is not currently available.'); + $this->expectExceptionMessage('The wishlist configuration is currently disabled.'); $query = <<<QUERY diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.php new file mode 100644 index 0000000000000..e452e70c24148 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistsTest.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\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +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\CollectionFactory; +use Magento\Wishlist\Model\Wishlist; + +/** + * Test coverage for customer wishlists + */ +class CustomerWishlistsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CollectionFactory + */ + private $wishlistCollectionFactory; + + /** + * Set Up + */ + protected function setUp(): void + { + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->wishlistCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + /** + * Test fetching customer wishlist + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php + */ + public function testCustomerWishlist(): void + { + $customerId = 1; + /** @var Wishlist $wishlist */ + $collection = $this->wishlistCollectionFactory->create()->filterByCustomerId($customerId); + /** @var Item $wishlistItem */ + $wishlistItem = $collection->getFirstItem(); + $response = $this->graphQlQuery( + $this->getQuery(), + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + $this->assertArrayHasKey('wishlists', $response['customer']); + $wishlist = $response['customer']['wishlists'][0]; + $this->assertEquals($wishlistItem->getItemsCount(), $wishlist['items_count']); + $this->assertEquals($wishlistItem->getSharingCode(), $wishlist['sharing_code']); + $this->assertEquals($wishlistItem->getUpdatedAt(), $wishlist['updated_at']); + $wishlistItemResponse = $wishlist['items_v2'][0]; + $this->assertEquals('simple', $wishlistItemResponse['product']['sku']); + } + + /** + * Testing fetching the wishlist when wishlist is disabled + * + * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCustomerCannotGetWishlistWhenDisabled(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('The wishlist configuration is currently disabled.'); + $this->graphQlQuery( + $this->getQuery(), + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + } + + /** + * Test wishlist fetching for a guest customer + * + * @magentoConfigFixture default_store wishlist/general/active 1 + */ + public function testGuestCannotGetWishlist(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('The current customer isn\'t authorized.'); + $this->graphQlQuery($this->getQuery()); + } + + /** + * Returns GraphQl query string + * + * @return string + */ + private function getQuery(): string + { + return <<<QUERY +query { + customer { + wishlists { + items_count + sharing_code + updated_at + items_v2 { + product { + sku + } + } + } + } +} +QUERY; + } + + /** + * Getting customer auth headers + * + * @param string $email + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php index ebe99289b8934..13aaecbc7b733 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -42,17 +42,17 @@ public function testDeleteWishlistItemFromWishlist(): void $wishlist = $this->getWishlist(); $wishlistId = $wishlist['customer']['wishlist']['id']; $wishlist = $wishlist['customer']['wishlist']; - $wishlistItems = $wishlist['items']; - self::assertEquals(1, $wishlist['items_count']); + $wishlistItems = $wishlist['items_v2']; + $this->assertEquals(1, $wishlist['items_count']); $query = $this->getQuery((int) $wishlistId, (int) $wishlistItems[0]['id']); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - self::assertArrayHasKey('removeProductsFromWishlist', $response); - self::assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']); + $this->assertArrayHasKey('removeProductsFromWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']); $wishlistResponse = $response['removeProductsFromWishlist']['wishlist']; - self::assertEquals(0, $wishlistResponse['items_count']); - self::assertEmpty($wishlistResponse['items']); + $this->assertEquals(0, $wishlistResponse['items_count']); + $this->assertEmpty($wishlistResponse['items_v2']); } /** @@ -98,10 +98,10 @@ private function getQuery( id sharing_code items_count - items { + items_v2 { id description - qty + quantity } } } @@ -134,9 +134,9 @@ private function getCustomerWishlistQuery(): string wishlist { id items_count - items { + items_v2 { id - qty + quantity description } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithUidForQueryBySku.php similarity index 94% rename from dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php rename to dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithUidForQueryBySku.php index fcba7458f317a..4bd0c135f039a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithUidForQueryBySku.php @@ -10,9 +10,9 @@ use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; /** - * Generate an array with test values for customizable options with encoded id_v2 value + * Generate an array with test values for customizable options with encoded uid value */ -class GetCustomOptionsWithIDV2ForQueryBySku +class GetCustomOptionsWithUidForQueryBySku { /** * @var ProductCustomOptionRepositoryInterface @@ -71,7 +71,7 @@ public function execute(string $sku): array } /** - * Returns id_v2 of the selected custom option + * Returns uid of the selected custom option * * @param int $optionId * @param int $optionValueId @@ -84,7 +84,7 @@ private function encodeSelectedOption(int $optionId, int $optionValueId): string } /** - * Returns id_v2 of the entered custom option + * Returns uid of the entered custom option * * @param int $optionId * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php index 9a9cd424e54ca..08273e7936640 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -43,18 +43,18 @@ public function testUpdateSimpleProductFromWishlist(): void $qty = 5; $description = 'New Description'; $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; - self::assertNotEquals($description, $wishlistItem['description']); - self::assertNotEquals($qty, $wishlistItem['qty']); + $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; + $this->assertNotEquals($description, $wishlistItem['description']); + $this->assertNotEquals($qty, $wishlistItem['quantity']); $query = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - self::assertArrayHasKey('updateProductsInWishlist', $response); - self::assertArrayHasKey('wishlist', $response['updateProductsInWishlist']); + $this->assertArrayHasKey('updateProductsInWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['updateProductsInWishlist']); $wishlistResponse = $response['updateProductsInWishlist']['wishlist']; - self::assertEquals($qty, $wishlistResponse['items'][0]['qty']); - self::assertEquals($description, $wishlistResponse['items'][0]['description']); + $this->assertEquals($qty, $wishlistResponse['items_v2'][0]['quantity']); + $this->assertEquals($description, $wishlistResponse['items_v2'][0]['description']); } /** @@ -110,10 +110,10 @@ private function getQuery( id sharing_code items_count - items { + items_v2 { id description - qty + quantity } } } @@ -146,9 +146,9 @@ private function getCustomerWishlistQuery(): string wishlist { id items_count - items { + items_v2 { id - qty + quantity description } } diff --git a/dev/tests/integration/etc/install-config-mysql.php.dist b/dev/tests/integration/etc/install-config-mysql.php.dist index 4766048c62375..1d4b3d1951e32 100644 --- a/dev/tests/integration/etc/install-config-mysql.php.dist +++ b/dev/tests/integration/etc/install-config-mysql.php.dist @@ -11,6 +11,9 @@ return [ 'db-name' => 'magento_integration_tests', 'db-prefix' => '', 'backend-frontname' => 'backend', + 'search-engine' => 'elasticsearch7', + 'elasticsearch-host' => 'localhost', + 'elasticsearch-port' => 9200, 'admin-user' => \Magento\TestFramework\Bootstrap::ADMIN_NAME, 'admin-password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, 'admin-email' => \Magento\TestFramework\Bootstrap::ADMIN_EMAIL, diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php index 2f4b7bf79c1d6..9172d7cf857e5 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/AbstractDataFixture.php @@ -7,8 +7,6 @@ namespace Magento\TestFramework\Annotation; -use Magento\Framework\Component\ComponentRegistrarInterface; -use Magento\Framework\Exception\LocalizedException; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use PHPUnit\Framework\Exception; use PHPUnit\Framework\TestCase; @@ -106,10 +104,18 @@ protected function _applyOneFixture($fixture) * Execute fixture scripts if any * * @param array $fixtures + * @param TestCase $test * @return void */ - protected function _applyFixtures(array $fixtures) + protected function _applyFixtures(array $fixtures, TestCase $test) { + /** @var \Magento\TestFramework\Annotation\TestsIsolation $testsIsolation */ + $testsIsolation = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\TestFramework\Annotation\TestsIsolation::class + ); + $dbIsolationState = $this->getDbIsolationState($test); + $testsIsolation->createDbSnapshot($test, $dbIsolationState); + /* Execute fixture scripts */ foreach ($fixtures as $oneFixture) { $this->_applyOneFixture($oneFixture); @@ -122,9 +128,10 @@ protected function _applyFixtures(array $fixtures) /** * Revert changes done by fixtures * + * @param TestCase|null $test * @return void */ - protected function _revertFixtures() + protected function _revertFixtures(?TestCase $test = null) { $resolver = Resolver::getInstance(); $resolver->setCurrentFixtureType($this->getAnnotation()); @@ -149,13 +156,22 @@ protected function _revertFixtures() } $this->_appliedFixtures = []; $resolver->setCurrentFixtureType(null); + + if (null !== $test) { + /** @var \Magento\TestFramework\Annotation\TestsIsolation $testsIsolation */ + $testsIsolation = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + \Magento\TestFramework\Annotation\TestsIsolation::class + ); + $dbIsolationState = $this->getDbIsolationState($test); + $testsIsolation->checkTestIsolation($test, $dbIsolationState); + } } /** * Return is explicit set isolation state * * @param TestCase $test - * @return bool|null + * @return array|null */ protected function getDbIsolationState(TestCase $test) { diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php index 02e53fc0a80ed..ffcdc186af520 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixture.php @@ -32,7 +32,7 @@ public function startTestTransactionRequest(TestCase $test, Transaction $param): if ($this->getDbIsolationState($test) !== ['disabled']) { $param->requestTransactionStart(); } else { - $this->_applyFixtures($fixtures); + $this->_applyFixtures($fixtures, $test); } } } @@ -51,7 +51,7 @@ public function endTestTransactionRequest(TestCase $test, Transaction $param): v if ($this->getDbIsolationState($test) !== ['disabled']) { $param->requestTransactionRollback(); } else { - $this->_revertFixtures(); + $this->_revertFixtures($test); } } } @@ -64,12 +64,13 @@ public function endTestTransactionRequest(TestCase $test, Transaction $param): v */ public function startTransaction(TestCase $test): void { - $this->_applyFixtures($this->_getFixtures($test)); + $this->_applyFixtures($this->_getFixtures($test), $test); } /** * Handler for 'rollbackTransaction' event * + * @param TestCase $test * @return void */ public function rollbackTransaction(): void diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php index 5685fea44f734..b36aebfd84728 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/DataFixtureBeforeTransaction.php @@ -24,7 +24,7 @@ public function startTest(TestCase $test) { $fixtures = $this->_getFixtures($test); if ($fixtures) { - $this->_applyFixtures($fixtures); + $this->_applyFixtures($fixtures, $test); } } @@ -37,7 +37,7 @@ public function endTest(TestCase $test) { /* Isolate other tests from test-specific fixtures */ if ($this->_appliedFixtures && $this->_getFixtures($test)) { - $this->_revertFixtures(); + $this->_revertFixtures($test); } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Annotation/TestsIsolation.php b/dev/tests/integration/framework/Magento/TestFramework/Annotation/TestsIsolation.php new file mode 100644 index 0000000000000..119ee1013a15c --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Annotation/TestsIsolation.php @@ -0,0 +1,194 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\TestFramework\Annotation; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ResourceConnection; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\AssertionFailedError; + +/** + * Validates tests isolation. Makes sure that test does not keep exceed data in DB. + */ +class TestsIsolation +{ + /** + * This variable was created to keep initial data cached + * + * @var array + */ + private $dbTableState = []; + + /** + * @var string[] + */ + private $testTypesToCheckIsolation = [ + 'integration', + ]; + + /** + * @var int + */ + private $isolationLevel = 0; + + /** + * @var string[] + */ + private $dbStateTables = [ + 'catalog_product_entity', + 'eav_attribute', + 'catalog_category_entity', + 'eav_attribute_set', + 'store', + 'store_website', + 'url_rewrite' + ]; + + /** + * Pull data from specific table + * + * @param string $table + * @return array + */ + private function pullDbState(string $table): array + { + $resource = ObjectManager::getInstance()->get(ResourceConnection::class); + $connection = $resource->getConnection(); + $select = $connection->select()->from($table); + return $connection->fetchAll($select); + } + + /** + * Create DB snapshot before test run. + * + * @param TestCase $test + * @param array|null $dbIsolationState + * @return void + */ + public function createDbSnapshot(TestCase $test, ?array $dbIsolationState): void + { + if (null !== $dbIsolationState + && ($dbIsolationState !== ['enabled']) + && ($this->checkIsolationRequired($test)) + ) { + ++$this->isolationLevel; + if ($this->isolationLevel === 1) { + $this->saveDbStateBeforeTestRun($test); + } + } + } + + /** + * Check DB isolation when test ended. + * + * @param TestCase $test + * @param array|null $dbIsolationState + * @return void + */ + public function checkTestIsolation(TestCase $test, ?array $dbIsolationState): void + { + if (null !== $dbIsolationState + && ($dbIsolationState !== ['enabled']) + && ($this->checkIsolationRequired($test)) + ) { + --$this->isolationLevel; + if ($this->isolationLevel === 1) { + $this->checkResidualData($test); + } + } + } + + /** + * Saving DB snapshot before fixtures applying. + * + * @param TestCase $test + * @return void + */ + private function saveDbStateBeforeTestRun(TestCase $test): void + { + try { + if (empty($this->dbTableState)) { + foreach ($this->dbStateTables as $table) { + $this->dbTableState[$table] = $this->pullDbState($table); + } + } + } catch (\Throwable $e) { + $test->getTestResultObject()->addFailure($test, new AssertionFailedError($e->getMessage()), 0); + } + } + + /** + * Check if test isolation is required for given scope of tests. + * + * @param TestCase $test + * @return bool + */ + private function checkIsolationRequired(TestCase $test): bool + { + $isRequired = false; + if (!$test->getTestResultObject()) { + return $isRequired; + } + + $testFilename = $test->getTestResultObject()->topTestSuite()->getName(); + foreach ($this->testTypesToCheckIsolation as $testType) { + if (false !== strpos($testFilename, \sprintf('/dev/tests/%s/', $testType))) { + $isRequired = true; + break; + } + } + + return $isRequired; + } + + /** + * Check if there's residual data in DB after test execution. + * + * @param TestCase $test + * @return void + */ + private function checkResidualData(TestCase $test): void + { + $isolationProblem = []; + foreach ($this->dbTableState as $table => $isolationData) { + try { + $diff = $this->dataDiff($isolationData, $this->pullDbState($table)); + if (!empty($diff)) { + $isolationProblem[$table] = $diff; + } + } catch (\Throwable $e) { + $test->getTestResultObject()->addFailure($test, new AssertionFailedError($e->getMessage()), 0); + } + } + + if (!empty($isolationProblem)) { + $test->getTestResultObject()->addFailure( + $test, + new AssertionFailedError( + "There was a problem with isolation: " . var_export($isolationProblem, true) + ), + 0 + ); + } + } + + /** + * Compare data difference for m-dimensional array + * + * @param array $dataBefore + * @param array $dataAfter + * @return array + */ + private function dataDiff(array $dataBefore, array $dataAfter): array + { + $diff = []; + if (count($dataBefore) !== count($dataAfter)) { + $diff = \array_slice($dataAfter, count($dataBefore)); + } + + return $diff; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php b/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php new file mode 100644 index 0000000000000..85007ad560d53 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Core/Version/View.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Core\Version; + +/** + * Class for magento version flag. + */ +class View +{ + /** + * Returns flag that checks that magento version is clean community version. + * + * @return bool + */ + public function isVersionUpdated(): bool + { + return false; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Indexer/TestCase.php b/dev/tests/integration/framework/Magento/TestFramework/Indexer/TestCase.php index b9a481e97c9a3..3360fa4342a5a 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Indexer/TestCase.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Indexer/TestCase.php @@ -7,7 +7,30 @@ class TestCase extends \PHPUnit\Framework\TestCase { + /** + * @var bool + */ + protected static $dbRestored = false; + + /** + * @inheritDoc + * + * @throws \Magento\Framework\Exception\LocalizedException + * @return void + */ public static function tearDownAfterClass(): void + { + if (empty(static::$dbRestored)) { + self::restoreFromDb(); + } + } + + /** + * Restore DB data after test execution. + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + protected static function restoreFromDb(): void { $db = \Magento\TestFramework\Helper\Bootstrap::getInstance()->getBootstrap() ->getApplication() 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 b3cfc2ae4fe79..3abe6ea4e061d 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 @@ -8,10 +8,12 @@ namespace Magento\Test\Annotation; use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Annotation\DataFixture; use Magento\TestFramework\Event\Param\Transaction; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Annotation\TestsIsolation; /** * Test class for \Magento\TestFramework\Annotation\DataFixture. @@ -25,6 +27,11 @@ class DataFixtureTest extends TestCase */ protected $object; + /** + * @var TestsIsolation|\PHPUnit\Framework\MockObject\MockObject + */ + protected $testsIsolationMock; + /** * @inheritdoc */ @@ -33,6 +40,18 @@ protected function setUp(): void $this->object = $this->getMockBuilder(DataFixture::class) ->setMethods(['_applyOneFixture', 'getComponentRegistrar', 'getTestKey']) ->getMock(); + $this->testsIsolationMock = $this->getMockBuilder(TestsIsolation::class) + ->setMethods(['createDbSnapshot', 'checkTestIsolation']) + ->getMock(); + /** @var ObjectManagerInterface|\PHPUnit\Framework\MockObject\MockObject $objectManager */ + $objectManager = $this->getMockBuilder(ObjectManagerInterface::class) + ->setMethods(['get']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $objectManager->expects($this->atLeastOnce())->method('get')->with(TestsIsolation::class) + ->willReturn($this->testsIsolationMock); + \Magento\TestFramework\Helper\Bootstrap::setObjectManager($objectManager); + $directory = __DIR__; if (!defined('INTEGRATION_TESTS_DIR')) { define('INTEGRATION_TESTS_DIR', dirname($directory, 4)); diff --git a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricingTest.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricingTest.php index 1ce2b01b10212..f6b8a06d2e16f 100644 --- a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricingTest.php +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricingTest.php @@ -7,6 +7,7 @@ namespace Magento\AdvancedPricingImportExport\Model\Export; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\File\Csv; use Magento\TestFramework\Indexer\TestCase; use Magento\TestFramework\Helper\Bootstrap; @@ -103,6 +104,8 @@ public function testExport() $this->assertEquals(count($origPricingData[$index]), count($newPricingData)); $this->assertEqualsOtherThanSkippedAttributes($origPricingData[$index], $newPricingData, []); } + + $this->removeImportedProducts($skus); } /** @@ -163,6 +166,7 @@ public function testExportMultipleWebsites() $this->assertEquals(count($origPricingData[$index]), count($newPricingData)); $this->assertEqualsOtherThanSkippedAttributes($origPricingData[$index], $newPricingData, []); } + $this->removeImportedProducts($skus); } /** @@ -173,14 +177,16 @@ public function testExportMultipleWebsites() */ public function testExportImportOfAdvancedPricing(): void { + $simpleSku = 'simple'; + $secondSimpleSku = 'second_simple'; $csvfile = uniqid('importexport_') . '.csv'; $exportContent = $this->exportData($csvfile); $this->assertStringContainsString( - 'second_simple,"All Websites [USD]","ALL GROUPS",10.0000,3.00,Discount', + \sprintf('%s,"All Websites [USD]","ALL GROUPS",10.0000,3.00,Discount', $secondSimpleSku), $exportContent ); $this->assertStringContainsString( - 'simple,"All Websites [USD]",General,5.0000,95.000000,Fixed', + \sprintf('%s,"All Websites [USD]",General,5.0000,95.000000,Fixed', $simpleSku), $exportContent ); $this->updateTierPriceDataInCsv($csvfile); @@ -224,6 +230,8 @@ public function testExportImportOfAdvancedPricing(): void ], 0.1 ); + + $this->removeImportedProducts([$simpleSku, $secondSimpleSku]); } /** @@ -331,4 +339,31 @@ private function assertEqualsOtherThanSkippedAttributes($expected, $actual, $ski } } } + + /** + * Cleanup test by removing imported product. + * + * @param string[] $skus + * @return void + */ + private function removeImportedProducts(array $skus): void + { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + /** @var ProductRepositoryInterface $productRepository */ + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + foreach ($skus as $sku) { + try { + $productRepository->deleteById($sku); + } catch (NoSuchEntityException $e) { + // product already deleted + } + } + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } } diff --git a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products_rollback.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products_rollback.php new file mode 100644 index 0000000000000..a814a7faea34b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/create_products_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\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Product $productModel + * @var ProductRepositoryInterface $productRepository + */ +$productModel = $objectManager->create(Product::class); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$skus = ['AdvancedPricingSimple 1', 'AdvancedPricingSimple 2']; +foreach ($skus as $sku) { + try { + $product = $productRepository->getById($sku); + $productRepository->delete($product); + } catch (NoSuchEntityException $exception) { + // product already removed + } +} diff --git a/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website_rollback.php new file mode 100644 index 0000000000000..c5678d3fdab4a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/AdvancedPricingImportExport/_files/product_with_second_website_rollback.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/website_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/AdvancedPricingImportExport/_files/create_products_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_tier_pricing_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_tier_pricing_rollback.php index 513c1fff62fb6..fc33758a9d01d 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_tier_pricing_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_tier_pricing_rollback.php @@ -13,7 +13,7 @@ * bundled items should not contain products with required custom options. * However, if to create such a bundle product, it will be always out of stock. */ -Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_rollback.php'); $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Framework\Registry $registry */ diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php index 361ceed5c02fe..89fe02f3dbc6c 100644 --- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php +++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php @@ -5,6 +5,8 @@ */ namespace Magento\BundleImportExport\Model\Import\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; @@ -34,6 +36,11 @@ class BundleTest extends \Magento\TestFramework\Indexer\TestCase */ protected $objectManager; + /** + * @var string[] + */ + private $importedProductSkus; + /** * List of Bundle options SKU * @@ -131,6 +138,7 @@ public function testBundleImport() } } } + $this->importedProductSkus = ['Simple 1', 'Simple 2', 'Simple 3', 'Bundle 1']; } /** @@ -192,6 +200,7 @@ public function testBundleImportWithMultipleStoreViews(): void } } } + $this->importedProductSkus = ['Simple 1', 'Simple 2', 'Simple 3', 'Bundle 1']; } /** @@ -199,6 +208,26 @@ public function testBundleImportWithMultipleStoreViews(): void */ protected function tearDown(): void { + if (!empty($this->importedProductSkus)) { + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->create(ProductRepositoryInterface::class); + $registry = $objectManager->get(\Magento\Framework\Registry::class); + /** @var ProductRepositoryInterface $productRepository */ + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + foreach ($this->importedProductSkus as $sku) { + try { + $productRepository->deleteById($sku); + } catch (NoSuchEntityException $e) { + // product already deleted + } + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + parent::tearDown(); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/OptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/OptionsTest.php new file mode 100644 index 0000000000000..c50c21a3328ae --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/OptionsTest.php @@ -0,0 +1,621 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset; + +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Block\Product\View\Options\AbstractRenderCustomOptionsTest; +use Magento\Catalog\Helper\Product as HelperProduct; +use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Option\Value; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\TestFramework\Helper\Xpath; + +/** + * Test cases related to check that simple product custom option renders as expected. + * + * @magentoAppArea adminhtml + */ +class OptionsTest extends AbstractRenderCustomOptionsTest +{ + /** @var HelperProduct */ + private $helperProduct; + + /** @var DataObjectFactory */ + private $dataObjectFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->helperProduct = $this->objectManager->get(HelperProduct::class); + $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php + * @return void + */ + public function testRenderCustomOptionsWithoutOptions(): void + { + $product = $this->productRepository->get('simple'); + $this->assertEquals( + 0, + Xpath::getElementsCountForXpath( + "//fieldset[@id='product_composite_configure_fields_options']", + $this->getOptionHtml($product) + ), + 'The option block is expected to be empty!' + ); + } + + /** + * Check that options from text group(field, area) render as expected. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php + * @dataProvider renderCustomOptionsFromTextGroupProvider + * @param array $optionData + * @param array $checkArray + * @return void + */ + public function testRenderCustomOptionsFromTextGroup(array $optionData, array $checkArray): void + { + $this->assertTextOptionRenderingOnProduct('simple', $optionData, $checkArray); + } + + /** + * Provides test data to verify the display of text type options. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function renderCustomOptionsFromTextGroupProvider(): array + { + return [ + 'type_text_required_field' => [ + [ + Option::KEY_TITLE => 'Test option type text 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 0, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'block_with_required_class' => '<div class="field admin__field">', + 'title' => 'Test option type text 1', + ], + 'equals_xpath' => [ + 'zero_price' => [ + 'xpath' => "//label[contains(@class, 'admin__field-label')]/span", + 'message' => 'Expected empty price is incorrect or missing!', + 'expected' => 0, + ], + ], + ], + ], + 'type_text_is_required_option' => [ + [ + Option::KEY_TITLE => 'Test option type text 2', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 1, + Option::KEY_PRICE => 0, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'block_with_required_class' => '<div class="field admin__field required _required">', + ], + ], + ], + 'type_text_fixed_positive_price' => [ + [ + Option::KEY_TITLE => 'Test option type text 3', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 50, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'price' => 'data-price-amount="50"', + ], + 'equals_xpath' => [ + 'sign_price' => [ + 'xpath' => "//label[contains(@class, 'admin__field-label')]/span[contains(text(), '+')]", + 'message' => 'Expected positive price is incorrect or missing!', + ], + ], + ], + ], + 'type_text_fixed_negative_price' => [ + [ + Option::KEY_TITLE => 'Test option type text 4', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => -50, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'price' => 'data-price-amount="50"', + ], + 'equals_xpath' => [ + 'sign_price' => [ + 'xpath' => "//label[contains(@class, 'admin__field-label')]/span[contains(text(), '-')]", + 'message' => 'Expected negative price is incorrect or missing!', + ], + ], + ], + ], + 'type_text_percent_price' => [ + [ + Option::KEY_TITLE => 'Test option type text 5', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 50, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_PERCENT, + Option::KEY_MAX_CHARACTERS => 0, + ], + [ + 'contains' => [ + 'price' => 'data-price-amount="5"', + ], + ], + ], + 'type_text_max_characters' => [ + [ + Option::KEY_TITLE => 'Test option type text 6', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 10, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 99, + ], + [ + 'max_characters' => (string)__('Maximum number of characters:') . ' <strong>99</strong>', + ], + ], + 'type_field' => [ + [ + Option::KEY_TITLE => 'Test option type field 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 10, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + 'configure_option_value' => 'Type field option value', + ], + [ + 'equals_xpath' => [ + 'control_price_attribute' => [ + 'xpath' => "//input[@id='options_%s_text' and @price='%s']", + 'message' => 'Expected input price is incorrect or missing!', + ], + 'default_option_value' => [ + 'xpath' => "//input[@id='options_%s_text' and @value='Type field option value']", + 'message' => 'Expected input default value is incorrect or missing!', + ], + ], + ], + ], + 'type_area' => [ + [ + Option::KEY_TITLE => 'Test option type area 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_AREA, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 10, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Option::KEY_MAX_CHARACTERS => 0, + 'configure_option_value' => 'Type area option value', + ], + [ + 'equals_xpath' => [ + 'control_price_attribute' => [ + 'xpath' => "//textarea[@id='options_%s_text' and @price='%s']", + 'message' => 'Expected textarea price is incorrect or missing!', + ], + 'default_option_value' => [ + 'xpath' => "//textarea[@id='options_%s_text' " + . "and contains(text(), 'Type area option value')]", + 'message' => 'Expected textarea default value is incorrect or missing!', + ], + ], + ], + ], + ]; + } + + /** + * Check that options from select group(drop-down, radio buttons, checkbox, multiple select) render as expected. + * + * @magentoDataFixture Magento/Catalog/_files/product_without_options_with_stock_data.php + * @dataProvider renderCustomOptionsFromSelectGroupProvider + * @param array $optionData + * @param array $optionValueData + * @param array $checkArray + * @return void + */ + public function testRenderCustomOptionsFromSelectGroup( + array $optionData, + array $optionValueData, + array $checkArray + ): void { + $this->assertSelectOptionRenderingOnProduct('simple', $optionData, $optionValueData, $checkArray); + } + + /** + * Provides test data to verify the display of select type options. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function renderCustomOptionsFromSelectGroupProvider(): array + { + return [ + 'type_select_required_field' => [ + [ + Option::KEY_TITLE => 'Test option type select 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + Option::KEY_IS_REQUIRE => 0, + ], + [ + Value::KEY_TITLE => 'Select value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'contains' => [ + 'block_with_required_class' => '<div class="admin__field field">', + 'title' => '<span>Test option type select 1</span>', + ], + 'equals_xpath' => [ + 'required_element' => [ + 'xpath' => "//select[@id='select_%s']", + 'message' => 'Expected select type is incorrect or missing!', + ], + ], + ], + ], + 'type_select_is_required_option' => [ + [ + Option::KEY_TITLE => 'Test option type select 2', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + Option::KEY_IS_REQUIRE => 1, + ], + [ + Value::KEY_TITLE => 'Select value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'contains' => [ + 'block_with_required_class' => '<div class="admin__field field _required">', + ], + ], + ], + 'type_drop_down_with_selected' => [ + [ + Option::KEY_TITLE => 'Test option type drop-down 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, + Option::KEY_IS_REQUIRE => 0, + 'configure_option_value' => 'Drop-down value 1', + ], + [ + Value::KEY_TITLE => 'Drop-down value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'element_type' => [ + 'xpath' => "//select[contains(@class, 'admin__control-select')]", + 'message' => 'Expected drop down type is incorrect or missing!', + ], + 'default_value' => [ + 'xpath' => "//option[contains(text(), '" . __('-- Please Select --') . "')]", + 'message' => 'Expected default value is incorrect or missing!', + ], + 'selected_value' => [ + 'xpath' => "//option[@selected='selected' and contains(text(), 'Drop-down value 1')]", + 'message' => 'Expected selected value is incorrect or missing!', + ], + ], + ], + ], + 'type_multiple_with_selected' => [ + [ + Option::KEY_TITLE => 'Test option type multiple 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_MULTIPLE, + Option::KEY_IS_REQUIRE => 0, + 'configure_option_value' => 'Multiple value 1', + ], + [ + Value::KEY_TITLE => 'Multiple value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'element_type' => [ + 'xpath' => "//select[contains(@class, 'admin__control-multiselect') " + . "and @multiple='multiple']", + 'message' => 'Expected multiple type is incorrect or missing!', + ], + 'selected_value' => [ + 'xpath' => "//option[@selected='selected' and contains(text(), 'Multiple value 1')]", + 'message' => 'Expected selected value is incorrect or missing!', + ], + ], + ], + ], + 'type_checkable_required_field' => [ + [ + Option::KEY_TITLE => 'Test option type checkable 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO, + Option::KEY_IS_REQUIRE => 0, + ], + [ + Value::KEY_TITLE => 'Checkable value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'required_checkable_option' => [ + 'xpath' => "//div[@id='options-%s-list']", + 'message' => 'Expected checkable option is incorrect or missing!', + ], + 'option_value_title' => [ + 'xpath' => "//label[@for='options_%s_2']/span[contains(text(), 'Checkable value 1')]", + 'message' => 'Expected option value title is incorrect or missing!', + ], + ], + ], + ], + 'type_radio_is_required_option' => [ + [ + Option::KEY_TITLE => 'Test option type radio 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO, + Option::KEY_IS_REQUIRE => 1, + ], + [ + Value::KEY_TITLE => 'Radio value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'span_container' => [ + 'xpath' => "//span[@id='options-%s-container']", + 'message' => 'Expected span container is incorrect or missing!', + ], + 'default_option_value' => [ + 'xpath' => "//label[@for='options_%s']/span[contains(text(), '" . __('None') . "')]", + 'message' => 'Expected default option value is incorrect or missing!', + 'expected' => 0, + ], + ], + ], + ], + 'type_radio_with_selected' => [ + [ + Option::KEY_TITLE => 'Test option type radio 2', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_RADIO, + Option::KEY_IS_REQUIRE => 0, + 'configure_option_value' => 'Radio value 1', + ], + [ + Value::KEY_TITLE => 'Radio value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + ], + [ + 'equals_xpath' => [ + 'default_option_value' => [ + 'xpath' => "//label[@for='options_%s']/span[contains(text(), '" . __('None') . "')]", + 'message' => 'Expected default option value is incorrect or missing!', + ], + 'element_type' => [ + 'xpath' => "//input[@id='options_%s_2' and contains(@class, 'admin__control-radio')]", + 'message' => 'Expected radio type is incorrect or missing!', + ], + 'selected_value' => [ + 'xpath' => "//input[@id='options_%s_2' and @checked='checked']", + 'message' => 'Expected selected option value is incorrect or missing!', + ], + ], + ], + ], + 'type_checkbox_is_required_option' => [ + [ + Option::KEY_TITLE => 'Test option type checkbox 1', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, + Option::KEY_IS_REQUIRE => 1, + ], + [ + Value::KEY_TITLE => 'Checkbox value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Value::KEY_SKU => '', + ], + [ + 'equals_xpath' => [ + 'span_container' => [ + 'xpath' => "//span[@id='options-%s-container']", + 'message' => 'Expected span container is incorrect or missing!', + ], + ], + ], + ], + 'type_checkbox_with_selected' => [ + [ + Option::KEY_TITLE => 'Test option type checkbox 2', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_CHECKBOX, + Option::KEY_IS_REQUIRE => 0, + 'configure_option_value' => 'Checkbox value 1', + ], + [ + Value::KEY_TITLE => 'Checkbox value 1', + Value::KEY_PRICE => 10, + Value::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_FIXED, + Value::KEY_SKU => '', + ], + [ + 'equals_xpath' => [ + 'element_type' => [ + 'xpath' => "//input[@id='options_%s_2' and contains(@class, 'admin__control-checkbox')]", + 'message' => 'Expected checkbox type is incorrect or missing!', + ], + 'selected_value' => [ + 'xpath' => "//input[@id='options_%s_2' and @checked='checked']", + 'message' => 'Expected selected option value is incorrect or missing!', + ], + ], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + protected function addOptionToProduct( + ProductInterface $product, + array $optionData, + array $optionValueData = [] + ): ProductInterface { + $product = parent::addOptionToProduct($product, $optionData, $optionValueData); + + if (isset($optionData['configure_option_value'])) { + $optionValue = $optionData['configure_option_value']; + $option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]); + if (!empty($optionValueData)) { + $optionValueObject = $this->findOptionValueByTitle($option, $optionValue); + $optionValue = $option->getType() === Option::OPTION_TYPE_CHECKBOX + ? [$optionValueObject->getOptionTypeId()] + : $optionValueObject->getOptionTypeId(); + } + /** @var DataObject $request */ + $buyRequest = $this->dataObjectFactory->create(); + $buyRequest->setData([ + 'qty' => 1, + 'options' => [$option->getId() => $optionValue], + ]); + $this->helperProduct->prepareProductOptions($product, $buyRequest); + } + + return $product; + } + + /** + * @inheritdoc + */ + protected function baseOptionAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + if (isset($checkArray['contains'])) { + foreach ($checkArray['contains'] as $needle) { + $this->assertStringContainsString($needle, $optionHtml); + } + } + } + + /** + * @inheritdoc + */ + protected function additionalTypeTextAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + parent::additionalTypeTextAsserts($option, $optionHtml, $checkArray); + + if (isset($checkArray['equals_xpath'])) { + foreach ($checkArray['equals_xpath'] as $key => $value) { + $value['args'] = $key === 'control_price_attribute' ? [(float)$option->getPrice()] : []; + $this->assertEqualsXpath($option, $optionHtml, $value); + } + } + } + + /** + * @inheritdoc + */ + protected function additionalTypeSelectAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + parent::additionalTypeSelectAsserts($option, $optionHtml, $checkArray); + + if (isset($checkArray['equals_xpath'])) { + foreach ($checkArray['equals_xpath'] as $value) { + $this->assertEqualsXpath($option, $optionHtml, $value); + } + } + } + + /** + * @inheritdoc + */ + protected function getHandlesList(): array + { + return [ + 'default', + 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE', + 'catalog_product_view_type_simple', + ]; + } + + /** + * @inheritdoc + */ + protected function getMaxCharactersCssClass(): string + { + return 'class="note"'; + } + + /** + * @inheritdoc + */ + protected function getOptionsBlockName(): string + { + return 'product.composite.fieldset.options'; + } + + /** + * Checks that the xpath string is equal to the expected value + * + * @param ProductCustomOptionInterface $option + * @param string $html + * @param array $xpathData + * @return void + */ + private function assertEqualsXpath(ProductCustomOptionInterface $option, string $html, array $xpathData): void + { + $args = array_merge([$option->getOptionId()], $xpathData['args'] ?? []); + $expected = $xpathData['expected'] ?? 1; + $this->assertEquals( + $expected, + Xpath::getElementsCountForXpath(sprintf($xpathData['xpath'], ...$args), $html), + $xpathData['message'] + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php new file mode 100644 index 0000000000000..a51b51a73645f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/Fieldset/QtyTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Product as HelperProduct; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test Qty block in composite product configuration layout + * + * @see \Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset\Qty + * @magentoAppArea adminhtml + */ +class QtyTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Qty */ + private $block; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var Registry */ + private $registry; + + /** @var HelperProduct */ + private $helperProduct; + + /** @var DataObjectFactory */ + private $dataObjectFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Qty::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->helperProduct = $this->objectManager->get(HelperProduct::class); + $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_product'); + $this->registry->unregister('product'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testGetProduct(): void + { + $product = $this->productRepository->get('simple-1'); + $this->registerProduct($product); + $this->assertEquals( + $product->getId(), + $this->block->getProduct()->getId(), + 'The expected product is missing in the Qty block!' + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @dataProvider getQtyValueProvider + * @param bool $isQty + * @param int $qty + * @return void + */ + public function testGetQtyValue(bool $isQty = false, int $qty = 1): void + { + $product = $this->productRepository->get('simple-1'); + if ($isQty) { + /** @var DataObject $request */ + $buyRequest = $this->dataObjectFactory->create(); + $buyRequest->setData(['qty' => $qty]); + $this->helperProduct->prepareProductOptions($product, $buyRequest); + } + $this->registerProduct($product); + $this->assertEquals($qty, $this->block->getQtyValue(), 'Expected block qty value is incorrect!'); + } + + /** + * Provides test data to verify block qty value. + * + * @return array + */ + public function getQtyValueProvider(): array + { + return [ + 'with_qty' => [ + 'is_qty' => true, + 'qty' => 5, + ], + 'without_qty' => [], + ]; + } + + /** + * Register the product + * + * @param ProductInterface $product + * @return void + */ + private function registerProduct(ProductInterface $product): void + { + $this->registry->unregister('current_product'); + $this->registry->unregister('product'); + $this->registry->register('current_product', $product); + $this->registry->register('product', $product); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php new file mode 100644 index 0000000000000..ab09314e18cc8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Composite/FieldsetTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Adminhtml\Product\Composite; + +use Magento\Backend\Model\View\Result\Page; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Test Fieldset block in composite product configuration layout + * + * @see \Magento\Catalog\Block\Adminhtml\Product\Composite\Fieldset + * @magentoAppArea adminhtml + */ +class FieldsetTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Page */ + private $page; + + /** @var Registry */ + private $registry; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var string */ + private $fieldsetXpath = "//fieldset[@id='product_composite_configure_fields_%s']"; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->page = $this->objectManager->get(PageFactory::class)->create(); + $this->registry = $this->objectManager->get(Registry::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_product'); + $this->registry->unregister('product'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @return void + */ + public function testRenderHtml(): void + { + $product = $this->productRepository->get('simple'); + $this->registerProduct($product); + $this->preparePage(); + $fieldsetBlock = $this->page->getLayout()->getBlock('product.composite.fieldset'); + $this->assertNotFalse($fieldsetBlock, 'Expected fieldset block is missing!'); + $html = $fieldsetBlock->toHtml(); + + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($this->fieldsetXpath, 'options'), $html), + 'Expected options block is missing!' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($this->fieldsetXpath, 'qty'), $html), + 'Expected qty block is missing!' + ); + } + + /** + * Prepare page layout + * + * @return void + */ + private function preparePage(): void + { + $this->page->addHandle([ + 'default', + 'CATALOG_PRODUCT_COMPOSITE_CONFIGURE', + 'catalog_product_view_type_simple', + ]); + $this->page->getLayout()->generateXml(); + } + + /** + * Register the product + * + * @param ProductInterface $product + * @return void + */ + private function registerProduct(ProductInterface $product): void + { + $this->registry->unregister('current_product'); + $this->registry->unregister('product'); + $this->registry->register('current_product', $product); + $this->registry->register('product', $product); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php index eb34696c70dbf..b575fc5e7033c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/AbstractRenderCustomOptionsTest.php @@ -9,14 +9,14 @@ use Magento\Catalog\Api\Data\ProductCustomOptionInterface; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterface; use Magento\Catalog\Api\Data\ProductCustomOptionValuesInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\View\Options; use Magento\Catalog\Model\Product\Option; -use Magento\Catalog\Model\Product\Option\Value; -use Magento\Framework\View\Element\Template; use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\TestCase; @@ -29,12 +29,12 @@ abstract class AbstractRenderCustomOptionsTest extends TestCase /** * @var ObjectManager */ - private $objectManager; + protected $objectManager; /** * @var ProductRepositoryInterface */ - private $productRepository; + protected $productRepository; /** * @var ProductCustomOptionInterfaceFactory @@ -57,12 +57,13 @@ abstract class AbstractRenderCustomOptionsTest extends TestCase protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); - $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); $this->productCustomOptionFactory = $this->objectManager->get(ProductCustomOptionInterfaceFactory::class); $this->productCustomOptionValuesFactory = $this->objectManager->get( ProductCustomOptionValuesInterfaceFactory::class ); - $this->page = $this->objectManager->create(Page::class); + $this->page = $this->objectManager->get(PageFactory::class)->create(); parent::setUp(); } @@ -94,11 +95,26 @@ protected function assertTextOptionRenderingOnProduct( $option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]); $optionHtml = $this->getOptionHtml($product); $this->baseOptionAsserts($option, $optionHtml, $checkArray); + $this->additionalTypeTextAsserts($option, $optionHtml, $checkArray); + } - if ($optionData[Option::KEY_MAX_CHARACTERS] > 0) { + /** + * Additional asserts for rendering text type options. + * + * @param ProductCustomOptionInterface $option + * @param string $optionHtml + * @param array $checkArray + * @return void + */ + protected function additionalTypeTextAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + if ($option->getMaxCharacters() > 0) { $this->assertStringContainsString($checkArray['max_characters'], $optionHtml); } else { - $this->assertStringNotContainsString('class="character-counter', $optionHtml); + $this->assertStringNotContainsString($this->getMaxCharactersCssClass(), $optionHtml); } } @@ -153,22 +169,36 @@ protected function assertSelectOptionRenderingOnProduct( $product = $this->productRepository->get($productSku); $product = $this->addOptionToProduct($product, $optionData, $optionValueData); $option = $this->findOptionByTitle($product, $optionData[Option::KEY_TITLE]); - $optionValues = $option->getValues(); - $optionValue = reset($optionValues); $optionHtml = $this->getOptionHtml($product); $this->baseOptionAsserts($option, $optionHtml, $checkArray); + $this->additionalTypeSelectAsserts($option, $optionHtml, $checkArray); + } + /** + * Additional asserts for rendering select type options. + * + * @param ProductCustomOptionInterface $option + * @param string $optionHtml + * @param array $checkArray + * @return void + */ + protected function additionalTypeSelectAsserts( + ProductCustomOptionInterface $option, + string $optionHtml, + array $checkArray + ): void { + $optionValues = $option->getValues(); + $optionValue = reset($optionValues); if (isset($checkArray['not_contain_arr'])) { foreach ($checkArray['not_contain_arr'] as $notContainPattern) { $this->assertDoesNotMatchRegularExpression($notContainPattern, $optionHtml); } } - if (isset($checkArray['option_value_item'])) { $checkArray['option_value_item'] = sprintf( $checkArray['option_value_item'], $optionValue->getOptionTypeId(), - $optionValueData[Value::KEY_TITLE] + $optionValue->getTitle() ); $this->assertMatchesRegularExpression($checkArray['option_value_item'], $optionHtml); } @@ -284,7 +314,7 @@ protected function assertDateOptionRenderingOnProduct( * @param array $checkArray * @return void */ - private function baseOptionAsserts( + protected function baseOptionAsserts( ProductCustomOptionInterface $option, string $optionHtml, array $checkArray @@ -317,7 +347,7 @@ private function baseOptionAsserts( * @param array $optionValueData * @return ProductInterface */ - private function addOptionToProduct( + protected function addOptionToProduct( ProductInterface $product, array $optionData, array $optionValueData = [] @@ -341,28 +371,16 @@ private function addOptionToProduct( * @param ProductInterface $product * @return string */ - private function getOptionHtml(ProductInterface $product): string - { - $optionsBlock = $this->getOptionsBlock(); - $optionsBlock->setProduct($product); - - return $optionsBlock->toHtml(); - } - - /** - * Get options block. - * - * @return Options - */ - private function getOptionsBlock(): Options + protected function getOptionHtml(ProductInterface $product): string { $this->page->addHandle($this->getHandlesList()); $this->page->getLayout()->generateXml(); - /** @var Template $productInfoFormOptionsBlock */ - $productInfoFormOptionsBlock = $this->page->getLayout()->getBlock('product.info.form.options'); - $optionsWrapperBlock = $productInfoFormOptionsBlock->getChildBlock('product_options_wrapper'); + /** @var Options $optionsBlock */ + $optionsBlock = $this->page->getLayout()->getBlock($this->getOptionsBlockName()); + $this->assertNotFalse($optionsBlock); + $optionsBlock->setProduct($product); - return $optionsWrapperBlock->getChildBlock('product_options'); + return $optionsBlock->toHtml(); } /** @@ -372,7 +390,7 @@ private function getOptionsBlock(): Options * @param string $optionTitle * @return null|Option */ - private function findOptionByTitle(ProductInterface $product, string $optionTitle): ?Option + protected function findOptionByTitle(ProductInterface $product, string $optionTitle): ?Option { $option = null; foreach ($product->getOptions() as $customOption) { @@ -385,10 +403,42 @@ private function findOptionByTitle(ProductInterface $product, string $optionTitl return $option; } + /** + * Find and return custom option value. + * + * @param ProductCustomOptionInterface $option + * @param string $optionValueTitle + * @return null|ProductCustomOptionValuesInterface + */ + protected function findOptionValueByTitle( + ProductCustomOptionInterface $option, + string $optionValueTitle + ): ?ProductCustomOptionValuesInterface { + $optionValue = null; + foreach ($option->getValues() as $customOptionValue) { + if ($customOptionValue->getTitle() === $optionValueTitle) { + $optionValue = $customOptionValue; + break; + } + } + + return $optionValue; + } + /** * Return all need handles for load. * * @return array */ abstract protected function getHandlesList(): array; + + /** + * @return string + */ + abstract protected function getMaxCharactersCssClass(): string; + + /** + * @return string + */ + abstract protected function getOptionsBlockName(): string; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php index da31cfc74476a..83c249ed062e6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/Options/RenderOptionsTest.php @@ -10,7 +10,6 @@ /** * Test cases related to check that simple product custom option renders as expected. * - * @magentoDbIsolation disabled * @magentoAppArea frontend */ class RenderOptionsTest extends AbstractRenderCustomOptionsTest @@ -89,4 +88,20 @@ protected function getHandlesList(): array 'catalog_product_view', ]; } + + /** + * @inheritdoc + */ + protected function getMaxCharactersCssClass(): string + { + return 'class="character-counter'; + } + + /** + * @inheritdoc + */ + protected function getOptionsBlockName(): string + { + return 'product.info.options'; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php index 0d2f9d63c5d7f..8c25a82e0f6fd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php @@ -15,6 +15,8 @@ use Magento\Catalog\Model\ResourceModel\Category\Tree; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Math\Random; use Magento\Framework\Url; use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\Store; @@ -419,6 +421,29 @@ public function testCategoryCreateWithDifferentFields(array $data): void $this->assertSame($data, $categoryData); } + /** + * Test for Category Description field to be able to contain >64kb of data + * + * @throws NoSuchEntityException + * @throws \Exception + */ + public function testMaximumDescriptionLength(): void + { + $random = Bootstrap::getObjectManager()->get(Random::class); + $longDescription = $random->getRandomString(70000); + + $requiredData = [ + 'name' => 'Test Category', + 'attribute_set_id' => '3', + 'parent_id' => 2, + 'description' => $longDescription + ]; + $this->_model->setData($requiredData); + $this->categoryResource->save($this->_model); + $category = $this->categoryRepository->get($this->_model->getId()); + $this->assertEquals($longDescription, $category->getDescription()); + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/_files/products_base_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/_files/products_base_rollback.php index 67bdb3d5c5e59..4515297fd1e8a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/_files/products_base_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Layer/Filter/Price/_files/products_base_rollback.php @@ -44,15 +44,6 @@ $lastProductId = 0; foreach ($testCases as $index => $testCase) { - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); - $position = $index + 1; - $categoryId = $index + 4; - $category->load($categoryId); - if ($category->getId()) { - $category->delete(); - } /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -74,3 +65,11 @@ ++$lastProductId; } } + +/** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ +$collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); +$collection + ->addAttributeToFilter('level', ['in' => [2, 3, 4]]) + ->load() + ->delete(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index b56e9e502cce6..b0f36f250991b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -8,14 +8,19 @@ namespace Magento\Catalog\Model; -use Magento\Eav\Model\Config as EavConfig; -use Magento\Catalog\Model\Product; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\TestFramework\ObjectManager; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\StateException; +use Magento\Framework\Math\Random; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; /** * Tests product model: @@ -119,14 +124,62 @@ public function testCRUD() )->setMetaDescription( 'meta description' )->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH + Visibility::VISIBILITY_BOTH )->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED + Status::STATUS_ENABLED ); $crud = new \Magento\TestFramework\Entity($this->_model, ['sku' => uniqid()]); $crud->testCrud(); } + /** + * Test for Product Description field to be able to contain >64kb of data + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoAppArea adminhtml + * @throws NoSuchEntityException + * @throws CouldNotSaveException + * @throws InputException + * @throws StateException + * @throws LocalizedException + */ + public function testMaximumDescriptionLength() + { + $sku = uniqid(); + $random = Bootstrap::getObjectManager()->get(Random::class); + $longDescription = $random->getRandomString(70000); + + $this->_model->setTypeId( + 'simple' + )->setAttributeSetId( + 4 + )->setName( + 'Simple Product With Long Description' + )->setDescription( + $longDescription + )->setSku( + $sku + )->setPrice( + 10 + )->setMetaTitle( + 'meta title' + )->setMetaKeyword( + 'meta keyword' + )->setMetaDescription( + 'meta description' + )->setVisibility( + Visibility::VISIBILITY_BOTH + )->setStatus( + Status::STATUS_ENABLED + ); + + $this->productRepository->save($this->_model); + $product = $this->productRepository->get($sku); + + $this->assertEquals($longDescription, $product->getDescription()); + } + /** * Test clean cache * @@ -219,7 +272,7 @@ public function testDuplicate() $this->assertNotEquals($duplicate->getId(), $this->_model->getId()); $this->assertNotEquals($duplicate->getSku(), $this->_model->getSku()); $this->assertEquals( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED, + Status::STATUS_DISABLED, $duplicate->getStatus() ); $this->assertEquals(\Magento\Store\Model\Store::DEFAULT_STORE_ID, $duplicate->getStoreId()); @@ -275,35 +328,35 @@ protected function _undo($duplicate) public function testVisibilityApi() { $this->assertEquals( - [\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED], + [Status::STATUS_ENABLED], $this->_model->getVisibleInCatalogStatuses() ); $this->assertEquals( - [\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED], + [Status::STATUS_ENABLED], $this->_model->getVisibleStatuses() ); - $this->_model->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED); + $this->_model->setStatus(Status::STATUS_DISABLED); $this->assertFalse($this->_model->isVisibleInCatalog()); - $this->_model->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + $this->_model->setStatus(Status::STATUS_ENABLED); $this->assertTrue($this->_model->isVisibleInCatalog()); $this->assertEquals( [ - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_IN_SEARCH, - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_IN_CATALOG, - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH, + Visibility::VISIBILITY_IN_SEARCH, + Visibility::VISIBILITY_IN_CATALOG, + Visibility::VISIBILITY_BOTH, ], $this->_model->getVisibleInSiteVisibilities() ); $this->assertFalse($this->_model->isVisibleInSiteVisibility()); - $this->_model->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_IN_SEARCH); + $this->_model->setVisibility(Visibility::VISIBILITY_IN_SEARCH); $this->assertTrue($this->_model->isVisibleInSiteVisibility()); - $this->_model->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_IN_CATALOG); + $this->_model->setVisibility(Visibility::VISIBILITY_IN_CATALOG); $this->assertTrue($this->_model->isVisibleInSiteVisibility()); - $this->_model->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH); + $this->_model->setVisibility(Visibility::VISIBILITY_BOTH); $this->assertTrue($this->_model->isVisibleInSiteVisibility()); } @@ -509,9 +562,9 @@ public function testValidate() )->setMetaDescription( 'meta description' )->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH + Visibility::VISIBILITY_BOTH )->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED + Status::STATUS_ENABLED )->setCollectExceptionMessages( true ); @@ -551,9 +604,9 @@ public function testValidateUniqueInputAttributeValue() $attribute->getAttributeCode(), 'unique value' )->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH + Visibility::VISIBILITY_BOTH )->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED + Status::STATUS_ENABLED )->setCollectExceptionMessages( true ); @@ -600,9 +653,9 @@ public function testValidateUniqueInputAttributeOnTheSameProduct() $attribute->getAttributeCode(), 'unique value' )->setVisibility( - \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH + Visibility::VISIBILITY_BOTH )->setStatus( - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED + Status::STATUS_ENABLED )->setCollectExceptionMessages( true ); @@ -675,10 +728,10 @@ public function testSaveWithBackordersEnabled(int $qty, int $stockStatus, bool $ * @magentoDataFixture Magento/Catalog/_files/product_simple.php * * @return void - * @throws \Magento\Framework\Exception\CouldNotSaveException - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\NoSuchEntityException - * @throws \Magento\Framework\Exception\StateException + * @throws CouldNotSaveException + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException */ public function testProductStatusWhenCatalogFlatProductIsEnabled() { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php new file mode 100644 index 0000000000000..68d5c43434daa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple Related Product') + ->setSku('simple') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) + ->setWebsiteIds([1]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) + ->save(); + +/** @var \Magento\Catalog\Api\Data\ProductLinkInterface $productLink */ +$productLink = $objectManager->create(\Magento\Catalog\Api\Data\ProductLinkInterface::class); +$productLink->setSku('simple_with_cross'); +$productLink->setLinkedProductSku('simple'); +$productLink->setPosition(1); +$productLink->setLinkType('related'); + +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setName('Simple Product With Related Product') + ->setSku('simple_with_cross') + ->setPrice(10) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) + ->setProductLinks([$productLink]) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php new file mode 100644 index 0000000000000..958398660b132 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_related_disabled_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +try { + $firstProduct = $productRepository->get('simple', false, null, true); + $productRepository->delete($firstProduct); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +try { + $secondProduct = $productRepository->get('simple_with_cross', false, null, true); + $productRepository->delete($secondProduct); +} 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/second_product_simple_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/second_product_simple_rollback.php index b045bfe4f6977..09e13c381aaed 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/second_product_simple_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/second_product_simple_rollback.php @@ -9,6 +9,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; use Magento\TestFramework\Helper\Bootstrap; +use Magento\UrlRewrite\Model\UrlRewrite; $objectManager = Bootstrap::getObjectManager(); /** @var Registry $registry */ @@ -26,5 +27,9 @@ //Product already removed } +$urlRewrite = $objectManager->create(UrlRewrite::class); +$urlRewrite->load('simple2.html', 'request_path'); +$urlRewrite->delete(); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/url_rewrites_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/url_rewrites_rollback.php index 1fa44427b3fbe..f671c43004ffa 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/url_rewrites_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/url_rewrites_rollback.php @@ -25,7 +25,7 @@ $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); $collection - ->addAttributeToFilter('level', 2) + ->addAttributeToFilter('name', ['in' => ['Old Root', 'Category 2', 'Category 1']]) ->load() ->delete(); 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 4502501da4f4f..a9699ea4a8050 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -1815,6 +1815,9 @@ public function testExistingProductWithUrlKeys() 'simple2' => 'url-key2', 'simple3' => 'url-key3' ]; + // added by _files/products_to_import_with_valid_url_keys.csv + $this->importedProducts[] = 'simple3'; + $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -1855,6 +1858,9 @@ public function testAddUpdateProductWithInvalidUrlKeys() : void 'simple2' => 'normal-url', 'simple3' => 'some!wrong\'url' ]; + // added by _files/products_to_import_with_invalid_url_keys.csv + $this->importedProducts[] = 'simple3'; + $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -2004,6 +2010,9 @@ public function testImportWithoutUrlKeys() 'simple2' => 'simple-2', 'simple3' => 'simple-3' ]; + // added by _files/products_to_import_without_url_keys.csv + $this->importedProducts[] = 'simple3'; + $filesystem = $this->objectManager->create(\Magento\Framework\Filesystem::class); $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); $source = $this->objectManager->create( @@ -2221,11 +2230,15 @@ function (ProductInterface $item) { $registry->register('isSecureArea', true); $productSkuList = ['simple1', 'simple2', 'simple3']; + $categoryIds = []; foreach ($productSkuList as $sku) { try { + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Model\Product $product */ $product = $productRepository->get($sku, true); + $categoryIds[] = $product->getCategoryIds(); if ($product->getId()) { $productRepository->delete($product); } @@ -2235,6 +2248,14 @@ function (ProductInterface $item) { } } + /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ + $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); + $collection + ->addAttributeToFilter('entity_id', ['in' => \array_unique(\array_merge(...$categoryIds))]) + ->load() + ->delete(); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); } @@ -3169,6 +3190,12 @@ public function testEmptyAttributeValueShouldBeIgnoredAfterUpdateProductByImport */ public function testCheckDoubleImportOfProducts() { + $this->importedProducts = [ + 'simple1', + 'simple2', + 'simple3', + ]; + /** @var SearchCriteria $searchCriteria */ $searchCriteria = $this->searchCriteriaBuilder->create(); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_sku_search_weight_score_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_sku_search_weight_score_rollback.php index 775d405654fdf..3622cecb7143d 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_sku_search_weight_score_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/products_for_sku_search_weight_score_rollback.php @@ -18,7 +18,16 @@ $productRepository = $objectManager->create(ProductRepositoryInterface::class); /** @var Registry $registry */ $registry = $objectManager->get(Registry::class); -$productSkus = ['1234-1234-1234-1234', 'Simple', 'product_with_description', 'product_with_attribute']; +$productSkus = [ + '1234-1234-1234-1234', + 'Simple', + 'product_with_description', + 'product_with_attribute', + 'nintendo-wii', + 'xbox', + 'console_description', + 'gamecube_attribute', +]; $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Fixtures/product_custom_url_key_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Fixtures/product_custom_url_key_rollback.php index 78b4f5ec238df..25fe62b91c6da 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Fixtures/product_custom_url_key_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Fixtures/product_custom_url_key_rollback.php @@ -5,18 +5,6 @@ */ declare(strict_types=1); -use Magento\Framework\Registry; -use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$objectManager = Bootstrap::getObjectManager(); - -/** @var Registry $registry */ -$registry = $objectManager->get(Registry::class); -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', true); - Resolver::getInstance()->requireDataFixture('Magento/CatalogUrlRewrite/_files/product_with_category_rollback.php'); - -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_rollback.php index 6be7354911654..fab5b173625d3 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_category_rollback.php @@ -46,6 +46,8 @@ $urlRewrite = $objectManager->create(UrlRewrite::class); $urlRewrite->load('non-exist-product.html', 'request_path'); $urlRewrite->delete(); +$urlRewrite->load('.html', 'request_path'); +$urlRewrite->delete(); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_stores_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_stores_rollback.php index 86f0ce34af00c..6b7d4072ead9a 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_stores_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/_files/product_with_stores_rollback.php @@ -6,9 +6,12 @@ declare(strict_types=1); use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; \Magento\TestFramework\Helper\Bootstrap::getInstance()->getInstance()->reinitialize(); +Resolver::getInstance()->requireDataFixture('Magento/CatalogUrlRewrite/_files/categories_with_stores_rollback.php'); + /** @var \Magento\Framework\Registry $registry */ $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php new file mode 100644 index 0000000000000..5cca93ce3478c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Type\Onepage; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var AddressInterface $quoteShippingAddress */ +$quoteShippingAddress = $objectManager->get(AddressInterfaceFactory::class)->create(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = $objectManager->get(AddressRepositoryInterface::class); +$quoteShippingAddress->importCustomerAddressData($addressRepository->getById(1)); +$customer = $customerRepository->getById(1); + +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId(1) + ->setIsActive(true) + ->setIsMultiShipping(0) + ->assignCustomerWithAddressChange($customer) + ->setShippingAddress($quoteShippingAddress) + ->setBillingAddress($quoteShippingAddress) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('55555555') + ->setEmail($customer->getEmail()); +$quote->addProduct($productRepository->get('simple-1'), 55); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getShippingAddress()->collectShippingRates(); +$quote->getPayment()->setMethod('checkmo'); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php new file mode 100644 index 0000000000000..a599d008cf89c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_ready_for_order_rollback.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\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('55555555'); +if ($quote) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_duplicated_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.php new file mode 100644 index 0000000000000..c74e76f74115f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer.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\Checkout\Model\Type\Onepage; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$customer = $customerRepository->get('customer@example.com'); + +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId(1) + ->setIsActive(false) + ->setIsMultiShipping(0) + ->setCustomer($customer) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('test_order_with_customer_inactive_quote') + ->addProduct($productRepository->get('taxable_product'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.php new file mode 100644 index 0000000000000..d45cbb547d29d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/inactive_quote_with_customer_rollback.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\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('test_order_with_customer_inactive_quote'); +if ($quote !== null) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/PageDesignTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/PageDesignTest.php index de1a78c87953c..9f8a620b8d2a5 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/PageDesignTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/PageDesignTest.php @@ -11,18 +11,21 @@ use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\GetPageByIdentifierInterface; use Magento\Cms\Model\Page; -use Magento\Cms\Model\PageFactory; use Magento\Framework\Acl\Builder; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; +use Magento\UrlRewrite\Model\UrlRewrite; /** * Test the saving CMS pages design via admin area interface. * * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PageDesignTest extends AbstractBackendController { @@ -77,6 +80,7 @@ protected function setUp(): void $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); $this->pageRetriever = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); $this->scopeConfig = Bootstrap::getObjectManager()->get(ScopeConfigInterface::class); + $this->pagesToDelete = []; } /** @@ -86,11 +90,40 @@ protected function tearDown(): void { parent::tearDown(); + $pageIds = []; foreach ($this->pagesToDelete as $identifier) { - $page = $this->pageRetriever->execute($identifier); + $pageIds[] = $identifier; + $page = $this->pageRetriever->execute($identifier, 0); $page->delete(); } - $this->pagesToDelete = []; + $this->removeUrlRewrites(); + } + + /** + * Removes url rewrites created during test execution. + * + * @return void + */ + private function removeUrlRewrites(): void + { + if (!empty($this->pagesToDelete)) { + /** @var UrlRewriteCollectionFactory $urlRewriteCollectionFactory */ + $urlRewriteCollectionFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + UrlRewriteCollectionFactory::class + ); + /** @var UrlRewriteCollection $urlRewriteCollection */ + $urlRewriteCollection = $urlRewriteCollectionFactory->create(); + $urlRewriteCollection->addFieldToFilter('request_path', ['in' => $this->pagesToDelete]); + $urlRewrites = $urlRewriteCollection->getItems(); + /** @var UrlRewrite $urlRewrite */ + foreach ($urlRewrites as $urlRewrite) { + try { + $urlRewrite->delete(); + } catch (\Exception $exception) { + // already removed + } + } + } } /** @@ -150,6 +183,7 @@ public function testSaveDesign(): void self::equalTo($sessionMessages), MessageInterface::TYPE_ERROR ); + $this->pagesToDelete = [$id]; } /** @@ -181,6 +215,7 @@ public function testSaveDesignWithDefaults(): void $this->assertNotEmpty($page->getId()); $this->assertNotNull($page->getPageLayout()); $this->assertEquals($defaultLayout, $page->getPageLayout()); + $this->pagesToDelete = [$id]; } /** @@ -227,5 +262,6 @@ public function testSaveLayoutXml(): void $updated = $this->pageRetriever->execute('test_custom_layout_page_1', 0); $this->assertEmpty($updated->getCustomLayoutUpdateXml()); $this->assertEmpty($updated->getLayoutUpdateXml()); + $this->pagesToDelete = ['test_custom_layout_page_1']; } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php new file mode 100644 index 0000000000000..076a669f3f8ad --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Cms\Model\Wysiwyg\Images; + +use Magento\Backend\Model\UrlInterface; +use Magento\Cms\Helper\Wysiwyg\Images as ImagesHelper; +use Magento\Framework\Url\EncoderInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class GetInsertImageContentTest extends TestCase +{ + /** + * @var GetInsertImageContent + */ + private $getInsertImageContent; + + /** + * @var ImagesHelper + */ + private $imagesHelper; + + /** + * @var EncoderInterface + */ + private $urlEncoder; + + /** + * @var UrlInterface + */ + protected $url; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->getInsertImageContent = Bootstrap::getObjectManager()->get(GetInsertImageContent::class); + $this->imagesHelper = Bootstrap::getObjectManager()->get(ImagesHelper::class); + $this->urlEncoder = Bootstrap::getObjectManager()->get(EncoderInterface::class); + $this->url = Bootstrap::getObjectManager()->get(UrlInterface::class); + } + + /** + * Test for GetInsertImageContent::execute + * + * @dataProvider imageDataProvider + * @param string $filename + * @param bool $forceStaticPath + * @param bool $renderAsTag + * @param int|null $storeId + * @param string $expectedResult + */ + public function testExecute( + string $filename, + bool $forceStaticPath, + bool $renderAsTag, + ?int $storeId, + string $expectedResult + ): void { + if (!$forceStaticPath && !$renderAsTag && !$this->imagesHelper->isUsingStaticUrlsAllowed()) { + $expectedResult = $this->url->getUrl( + 'cms/wysiwyg/directive', + [ + '___directive' => $this->urlEncoder->encode($expectedResult), + '_escape_params' => false + ] + ); + } + + $this->assertEquals( + $expectedResult, + $this->getInsertImageContent->execute( + $this->imagesHelper->idEncode($filename), + $forceStaticPath, + $renderAsTag, + $storeId + ) + ); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function imageDataProvider(): array + { + return [ + [ + 'test-image.jpg', + false, + true, + 1, + '<img src="{{media url="test-image.jpg"}}" alt="" />' + ], + [ + 'catalog/category/test-image.jpg', + true, + false, + 1, + '/pub/media/catalog/category/test-image.jpg' + ], + [ + 'test-image.jpg', + false, + false, + 1, + '{{media url="test-image.jpg"}}' + ], + [ + '/test-image.jpg', + false, + true, + 2, + '<img src="{{media url="/test-image.jpg"}}" alt="" />' + ], + [ + 'test-image.jpg', + false, + true, + null, + '<img src="{{media url="test-image.jpg"}}" alt="" />' + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php new file mode 100644 index 0000000000000..88c8fb726c472 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/ConfigurableTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test Configurable block in composite product configuration layout + * + * @see \Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset\Configurable + * @magentoAppArea adminhtml + */ +class ConfigurableTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var SerializerInterface */ + private $serializer; + + /** @var Configurable */ + private $block; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var Registry */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->serializer = $this->objectManager->get(SerializerInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Configurable::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('product'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testGetProduct(): void + { + $product = $this->productRepository->get('simple-1'); + $this->registerProduct($product); + $blockProduct = $this->block->getProduct(); + $this->assertSame($product, $blockProduct); + $this->assertEquals( + $product->getId(), + $blockProduct->getId(), + 'The expected product is missing in the Configurable block!' + ); + $this->assertNotNull($blockProduct->getTypeInstance()->getStoreFilter($blockProduct)); + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @return void + */ + public function testGetJsonConfig(): void + { + $product = $this->productRepository->get('configurable'); + $this->registerProduct($product); + $config = $this->serializer->unserialize($this->block->getJsonConfig()); + $this->assertTrue($config['disablePriceReload']); + $this->assertTrue($config['stablePrices']); + } + + /** + * Register the product + * + * @param ProductInterface $product + * @return void + */ + private function registerProduct(ProductInterface $product): void + { + $this->registry->unregister('product'); + $this->registry->register('product', $product); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php index 55f8b91f07093..303a32d34bf6c 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/CustomOptions/RenderOptionsTest.php @@ -12,7 +12,6 @@ /** * Test cases related to check that configurable product custom option renders as expected. * - * @magentoDbIsolation disabled * @magentoAppArea frontend */ class RenderOptionsTest extends AbstractRenderCustomOptionsTest @@ -93,4 +92,20 @@ protected function getHandlesList(): array 'catalog_product_view_type_configurable', ]; } + + /** + * @inheritdoc + */ + protected function getMaxCharactersCssClass(): string + { + return 'class="character-counter'; + } + + /** + * @inheritdoc + */ + protected function getOptionsBlockName(): string + { + return 'product.info.options'; + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php index 39ed7965ea9e9..0344d467a3cc2 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php @@ -9,9 +9,13 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Product as HelperProduct; use Magento\Catalog\Model\ResourceModel\Product as ProductResource; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\View\LayoutInterface; @@ -26,6 +30,7 @@ * @magentoAppIsolation enabled * @magentoDbIsolation enabled * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigurableTest extends TestCase { @@ -64,6 +69,14 @@ class ConfigurableTest extends TestCase */ private $product; + /** + * @var HelperProduct + */ + private $helperProduct; + + /** @var DataObjectFactory */ + private $dataObjectFactory; + /** * @inheritdoc */ @@ -79,6 +92,8 @@ protected function setUp(): void $this->product = $this->productRepository->get('configurable'); $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Configurable::class); $this->block->setProduct($this->product); + $this->helperProduct = $this->objectManager->get(HelperProduct::class); + $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class); } /** @@ -128,6 +143,29 @@ public function testGetJsonConfig(): void $this->assertCount(0, $config['images']); } + /** + * @return void + */ + public function testGetJsonConfigWithPreconfiguredValues(): void + { + /** @var ConfigurableAttribute $attribute */ + $attribute = $this->product->getExtensionAttributes()->getConfigurableProductOptions()[0]; + $expectedAttributeValue = [ + $attribute->getAttributeId() => $attribute->getOptions()[0]['value_index'], + ]; + /** @var DataObject $request */ + $buyRequest = $this->dataObjectFactory->create(); + $buyRequest->setData([ + 'qty' => 1, + 'super_attribute' => $expectedAttributeValue, + ]); + $this->helperProduct->prepareProductOptions($this->product, $buyRequest); + + $config = $this->serializer->unserialize($this->block->getJsonConfig()); + $this->assertArrayHasKey('defaultValues', $config); + $this->assertEquals($expectedAttributeValue, $config['defaultValues']); + } + /** * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_child_products_with_images.php * @return void diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php index 9c382068ceebc..12585992d084c 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Address/EditTest.php @@ -3,126 +3,175 @@ * 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\AddressRegistry; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + /** * Tests Address Edit Block + * + * @magentoAppArea frontend + * @magentoAppIsolation enabled */ -class EditTest extends \PHPUnit\Framework\TestCase +class EditTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + /** @var Edit */ - protected $_block; + private $block; - /** @var \Magento\Customer\Model\Session */ - protected $_customerSession; + /** @var Session */ + private $customerSession; - /** @var \Magento\Backend\Block\Template\Context */ - protected $_context; + /** @var AddressRegistry */ + private $addressRegistry; - /** @var string */ - protected $_requestId; + /** @var CustomerRegistry */ + private $customerRegistry; + /** @var RequestInterface */ + private $request; + + /** + * @inheritdoc + */ protected function setUp(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $this->_customerSession = $objectManager->get(\Magento\Customer\Model\Session::class); - $this->_customerSession->setCustomerId(1); - - $this->_context = $objectManager->get(\Magento\Backend\Block\Template\Context::class); - $this->_requestId = $this->_context->getRequest()->getParam('id'); - $this->_context->getRequest()->setParam('id', '1'); - - $objectManager->get(\Magento\Framework\App\State::class)->setAreaCode('frontend'); - - /** @var $layout \Magento\Framework\View\Layout */ - $layout = $objectManager->get(\Magento\Framework\View\LayoutInterface::class); - $currentCustomer = $objectManager->create( - \Magento\Customer\Helper\Session\CurrentCustomer::class, - ['customerSession' => $this->_customerSession] - ); - $this->_block = $layout->createBlock( - \Magento\Customer\Block\Address\Edit::class, - '', - ['customerSession' => $this->_customerSession, 'currentCustomer' => $currentCustomer] - ); + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(Session::class); + $this->customerSession->setCustomerId(1); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->request->setParam('id', '1'); + /** @var Page $page */ + $page = $this->objectManager->get(PageFactory::class)->create(); + $page->addHandle(['default', 'customer_address_form']); + $page->getLayout()->generateXml(); + $this->block = $page->getLayout()->getBlock('customer_address_edit'); + $this->addressRegistry = $this->objectManager->get(AddressRegistry::class); + $this->customerRegistry = $this->objectManager->get(CustomerRegistry::class); } + /** + * @inheritdoc + */ protected function tearDown(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_customerSession->setCustomerId(null); - $this->_context->getRequest()->setParam('id', $this->_requestId); - /** @var \Magento\Customer\Model\AddressRegistry $addressRegistry */ - $addressRegistry = $objectManager->get(\Magento\Customer\Model\AddressRegistry::class); + parent::tearDown(); + $this->customerSession->setCustomerId(null); + $this->request->setParam('id', null); //Cleanup address from registry - $addressRegistry->remove(1); - $addressRegistry->remove(2); - - /** @var \Magento\Customer\Model\CustomerRegistry $customerRegistry */ - $customerRegistry = $objectManager->get(\Magento\Customer\Model\CustomerRegistry::class); + $this->addressRegistry->remove(1); + $this->addressRegistry->remove(2); //Cleanup customer from registry - $customerRegistry->remove(1); + $this->customerRegistry->remove(1); } /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void */ - public function testGetSaveUrl() + public function testGetSaveUrl(): void { - $this->assertEquals('http://localhost/index.php/customer/address/formPost/', $this->_block->getSaveUrl()); + $this->assertEquals('http://localhost/index.php/customer/address/formPost/', $this->block->getSaveUrl()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @return void */ - public function testGetRegionId() + public function testGetRegionId(): void { - $this->assertEquals(1, $this->_block->getRegionId()); + $this->assertEquals(1, $this->block->getRegionId()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @return void */ - public function testGetCountryId() + public function testGetCountryId(): void { - $this->assertEquals('US', $this->_block->getCountryId()); + $this->assertEquals('US', $this->block->getCountryId()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_two_addresses.php + * @return void */ - public function testGetCustomerAddressCount() + public function testGetCustomerAddressCount(): void { - $this->assertEquals(2, $this->_block->getCustomerAddressCount()); + $this->assertEquals(2, $this->block->getCustomerAddressCount()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void */ - public function testCanSetAsDefaultShipping() + public function testCanSetAsDefaultShipping(): void { - $this->assertEquals(0, $this->_block->canSetAsDefaultShipping()); + $this->assertEquals(0, $this->block->canSetAsDefaultShipping()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @return void */ - public function testIsDefaultBilling() + public function testIsDefaultBilling(): void { - $this->assertFalse($this->_block->isDefaultBilling()); + $this->assertFalse($this->block->isDefaultBilling()); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Customer/_files/customer_address.php + * @return void + */ + public function testGetStreetLine(): void + { + $this->assertEquals('Green str, 67', $this->block->getStreetLine(1)); + $this->assertEquals('', $this->block->getStreetLine(2)); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/create_account/vat_frontend_visibility 1 + * @return void + */ + public function testVatIdFieldVisible(): void + { + $html = $this->block->toHtml(); + $labelXpath = "//div[contains(@class, 'taxvat')]//label/span[normalize-space(text()) = '%s']"; + $this->assertEquals(1, Xpath::getElementsCountForXpath(sprintf($labelXpath, __('VAT Number')), $html)); + $inputXpath = "//div[contains(@class, 'taxvat')]//div/input[contains(@id,'vat_id') and @type='text']"; + $this->assertEquals(1, Xpath::getElementsCountForXpath($inputXpath, $html)); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store customer/create_account/vat_frontend_visibility 0 + * @return void */ - public function testGetStreetLine() + public function testVatIdFieldNotVisible(): void { - $this->assertEquals('Green str, 67', $this->_block->getStreetLine(1)); - $this->assertEquals('', $this->_block->getStreetLine(2)); + $html = $this->block->toHtml(); + $labelXpath = "//div[contains(@class, 'taxvat')]//label/span[normalize-space(text()) = '%s']"; + $this->assertEquals(0, Xpath::getElementsCountForXpath(sprintf($labelXpath, __('VAT Number')), $html)); + $inputXpath = "//div[contains(@class, 'taxvat')]//div/input[contains(@id,'vat_id') and @type='text']"; + $this->assertEquals(0, Xpath::getElementsCountForXpath($inputXpath, $html)); } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php new file mode 100644 index 0000000000000..8ce1d2ae9ccf9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Account/CreatePostTest.php @@ -0,0 +1,303 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Controller\Account; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\App\Http; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; +use Magento\Theme\Controller\Result\MessagePlugin; + +/** + * Tests from customer account create post action. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreatePostTest extends AbstractController +{ + /** + * @var TransportBuilderMock + */ + private $transportBuilderMock; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CookieManagerInterface + */ + private $cookieManager; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->transportBuilderMock = $this->_objectManager->get(TransportBuilderMock::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + $this->customerRegistry = $this->_objectManager->get(CustomerRegistry::class); + $this->cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $this->urlBuilder = $this->_objectManager->get(UrlInterface::class); + } + + /** + * Tests that without form key user account won't be created + * and user will be redirected on account creation page again. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @return void + */ + public function testNoFormKeyCreatePostAction(): void + { + $this->fillRequestWithAccountData('test1@email.com'); + $this->getRequest()->setPostValue('form_key', null); + $this->dispatch('customer/account/createPost'); + + $this->assertCustomerNotExists('test1@email.com'); + $this->assertRedirect($this->stringEndsWith('customer/account/create/')); + $this->assertSessionMessages( + $this->stringContains((string)__('Invalid Form Key. Please refresh the page.')), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture current_website customer/create_account/confirm 0 + * @magentoConfigFixture current_store customer/create_account/default_group 1 + * @magentoConfigFixture current_store customer/create_account/generate_human_friendly_id 0 + * + * @return void + */ + public function testNoConfirmCreatePostAction(): void + { + $this->fillRequestWithAccountData('test1@email.com'); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringEndsWith('customer/account/')); + $this->assertSessionMessages( + $this->containsEqual( + (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()) + ), + MessageInterface::TYPE_SUCCESS + ); + $customer = $this->customerRegistry->retrieveByEmail('test1@email.com'); + //Assert customer group + $this->assertEquals(1, $customer->getDataModel()->getGroupId()); + //Assert customer increment id generation + $this->assertNull($customer->getData('increment_id')); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture current_website customer/create_account/confirm 0 + * @magentoConfigFixture current_store customer/create_account/default_group 2 + * @magentoConfigFixture current_store customer/create_account/generate_human_friendly_id 1 + * @return void + */ + public function testCreatePostWithCustomConfiguration(): void + { + $this->fillRequestWithAccountData('test@email.com'); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringEndsWith('customer/account/')); + $this->assertSessionMessages( + $this->containsEqual( + (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()) + ), + MessageInterface::TYPE_SUCCESS + ); + $customer = $this->customerRegistry->retrieveByEmail('test@email.com'); + //Assert customer group + $this->assertEquals(2, $customer->getDataModel()->getGroupId()); + //Assert customer increment id generation + $this->assertNotNull($customer->getData('increment_id')); + $this->assertMatchesRegularExpression('/\d{8}/', $customer->getData('increment_id')); + } + + /** + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoConfigFixture current_website customer/create_account/confirm 1 + * + * @return void + */ + public function testWithConfirmCreatePostAction(): void + { + $email = 'test2@email.com'; + $this->fillRequestWithAccountData($email); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringContains('customer/account/index/')); + $message = 'You must confirm your account.' + . ' Please check your email for the confirmation link or <a href="%1">click here</a> for a new link.'; + $url = $this->urlBuilder->getUrl('customer/account/confirmation', ['_query' => ['email' => $email]]); + $this->assertSessionMessages( + $this->containsEqual((string)__($message, $url)), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testExistingEmailCreatePostAction(): void + { + $this->fillRequestWithAccountData('customer@example.com'); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringContains('customer/account/create/')); + $message = 'There is already an account with this email address.' + . ' If you are sure that it is your email address, <a href="%1">click here</a> ' + . 'to get your password and access your account.'; + $url = $this->urlBuilder->getUrl('customer/account/forgotpassword'); + $this->assertSessionMessages($this->containsEqual((string)__($message, $url)), MessageInterface::TYPE_ERROR); + } + + /** + * Register Customer with email confirmation. + * + * @magentoAppArea frontend + * @magentoConfigFixture current_website customer/create_account/confirm 1 + * + * @return void + */ + public function testRegisterCustomerWithEmailConfirmation(): void + { + $email = 'test_example@email.com'; + $this->fillRequestWithAccountData($email); + $this->dispatch('customer/account/createPost'); + $this->assertRedirect($this->stringContains('customer/account/index/')); + $message = 'You must confirm your account.' + . ' Please check your email for the confirmation link or <a href="%1">click here</a> for a new link.'; + $url = $this->urlBuilder->getUrl('customer/account/confirmation', ['_query' => ['email' => $email]]); + $this->assertSessionMessages($this->containsEqual((string)__($message, $url)), MessageInterface::TYPE_SUCCESS); + /** @var CustomerInterface $customer */ + $customer = $this->customerRepository->get($email); + $confirmation = $customer->getConfirmation(); + $sendMessage = $this->transportBuilderMock->getSentMessage(); + $this->assertNotNull($sendMessage); + $rawMessage = $sendMessage->getBody()->getParts()[0]->getRawContent(); + $this->assertStringContainsString( + (string)__( + 'You must confirm your %customer_email email before you can sign in (link is only valid once):', + ['customer_email' => $email] + ), + $rawMessage + ); + $this->assertStringContainsString( + sprintf('customer/account/confirm/?id=%s&key=%s', $customer->getId(), $confirmation), + $rawMessage + ); + $this->resetRequest(); + $this->getRequest() + ->setParam('id', $customer->getId()) + ->setParam('key', $confirmation); + $this->dispatch('customer/account/confirm'); + $this->assertRedirect($this->stringContains('customer/account/index/')); + $this->assertSessionMessages( + $this->containsEqual( + (string)__('Thank you for registering with %1.', $this->storeManager->getStore()->getFrontendName()) + ), + MessageInterface::TYPE_SUCCESS + ); + $this->assertEmpty($this->customerRepository->get($email)->getConfirmation()); + } + + /** + * Fills request with customer data. + * + * @param string $email + * @return void + */ + private function fillRequestWithAccountData(string $email): void + { + $this->getRequest() + ->setMethod(HttpRequest::METHOD_POST) + ->setParam(CustomerInterface::FIRSTNAME, 'firstname1') + ->setParam(CustomerInterface::LASTNAME, 'lastname1') + ->setParam(CustomerInterface::EMAIL, $email) + ->setParam('password', '_Password1') + ->setParam('password_confirmation', '_Password1') + ->setParam('telephone', '5123334444') + ->setParam('street', ['1234 fake street', '']) + ->setParam('city', 'Austin') + ->setParam('postcode', '78701') + ->setParam('country_id', 'US') + ->setParam('default_billing', '1') + ->setParam('default_shipping', '1') + ->setParam('is_subscribed', '0') + ->setPostValue('create_address', true); + } + + /** + * Asserts that customer does not exists. + * + * @param string $email + * @return void + */ + private function assertCustomerNotExists(string $email): void + { + $this->expectException(NoSuchEntityException::class); + $this->expectExceptionMessage( + (string)__( + 'No such entity with %fieldName = %fieldValue, %field2Name = %field2Value', + [ + 'fieldName' => 'email', + 'fieldValue' => $email, + 'field2Name' => 'websiteId', + 'field2Value' => 1 + ] + ) + ); + $this->assertNull($this->customerRepository->get($email)); + } + + /** + * Clears request. + * + * @return void + */ + protected function resetRequest(): void + { + parent::resetRequest(); + $this->cookieManager->deleteCookie(MessagePlugin::MESSAGES_COOKIES_NAME); + $this->_objectManager->removeSharedInstance(Http::class); + $this->_objectManager->removeSharedInstance(Request::class); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index 5527a39ce0507..6abbff18c645c 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -10,13 +10,10 @@ use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\CustomerRegistry; use Magento\Customer\Model\Session; -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Http; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Data\Form\FormKey; use Magento\Framework\Message\MessageInterface; -use Magento\Framework\Phrase; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\CookieManagerInterface; use Magento\Store\Model\StoreManager; @@ -220,83 +217,6 @@ public function testConfirmActionAlreadyActive() $this->getResponse()->getBody(); } - /** - * Tests that without form key user account won't be created - * and user will be redirected on account creation page again. - */ - public function testNoFormKeyCreatePostAction() - { - $this->fillRequestWithAccountData('test1@email.com'); - $this->getRequest()->setPostValue('form_key', null); - $this->dispatch('customer/account/createPost'); - - $this->assertNull($this->getCustomerByEmail('test1@email.com')); - $this->assertRedirect($this->stringEndsWith('customer/account/create/')); - $this->assertSessionMessages( - $this->equalTo([new Phrase('Invalid Form Key. Please refresh the page.')]), - MessageInterface::TYPE_ERROR - ); - } - - /** - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_disable.php - */ - public function testNoConfirmCreatePostAction() - { - $this->fillRequestWithAccountDataAndFormKey('test1@email.com'); - $this->dispatch('customer/account/createPost'); - $this->assertRedirect($this->stringEndsWith('customer/account/')); - $this->assertSessionMessages( - $this->equalTo(['Thank you for registering with Main Website Store.']), - MessageInterface::TYPE_SUCCESS - ); - } - - /** - * @magentoDbIsolation enabled - * @magentoAppIsolation enabled - * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php - */ - public function testWithConfirmCreatePostAction() - { - $this->fillRequestWithAccountDataAndFormKey('test2@email.com'); - $this->dispatch('customer/account/createPost'); - $this->assertRedirect($this->stringContains('customer/account/index/')); - $this->assertSessionMessages( - $this->equalTo( - [ - 'You must confirm your account. Please check your email for the confirmation link or ' - . '<a href="http://localhost/index.php/customer/account/confirmation/' - . '?email=test2%40email.com">click here</a> for a new link.' - ] - ), - MessageInterface::TYPE_SUCCESS - ); - } - - /** - * @magentoDataFixture Magento/Customer/_files/customer.php - */ - public function testExistingEmailCreatePostAction() - { - $this->fillRequestWithAccountDataAndFormKey('customer@example.com'); - $this->dispatch('customer/account/createPost'); - $this->assertRedirect($this->stringContains('customer/account/create/')); - $this->assertSessionMessages( - $this->equalTo( - [ - 'There is already an account with this email address. ' . - 'If you are sure that it is your email address, ' . - '<a href="http://localhost/index.php/customer/account/forgotpassword/">click here</a>' . - ' to get your password and access your account.', - ] - ), - MessageInterface::TYPE_ERROR - ); - } - /** * @magentoDataFixture Magento/Customer/_files/inactive_customer.php */ @@ -613,70 +533,6 @@ public function testWrongConfirmationEditPostAction() ); } - /** - * Register Customer with email confirmation. - * - * @magentoDataFixture Magento/Customer/_files/customer_confirmation_config_enable.php - * @return void - * @throws \Magento\Framework\Exception\InputException - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Magento\Framework\Exception\NoSuchEntityException - * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException - */ - public function testRegisterCustomerWithEmailConfirmation(): void - { - $email = 'test_example@email.com'; - $this->fillRequestWithAccountDataAndFormKey($email); - $this->dispatch('customer/account/createPost'); - $this->assertRedirect($this->stringContains('customer/account/index/')); - $this->assertSessionMessages( - $this->equalTo( - [ - 'You must confirm your account. Please check your email for the confirmation link or ' - . '<a href="http://localhost/index.php/customer/account/confirmation/' - . '?email=test_example%40email.com">click here</a> for a new link.' - ] - ), - MessageInterface::TYPE_SUCCESS - ); - /** @var CustomerRepositoryInterface $customerRepository */ - $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); - /** @var CustomerInterface $customer */ - $customer = $customerRepository->get($email); - $confirmation = $customer->getConfirmation(); - $message = $this->transportBuilderMock->getSentMessage(); - $rawMessage = $message->getBody()->getParts()[0]->getRawContent(); - $messageConstraint = $this->logicalAnd( - new StringContains("You must confirm your {$email} email before you can sign in (link is only valid once"), - new StringContains("customer/account/confirm/?id={$customer->getId()}&key={$confirmation}") - ); - $this->assertThat($rawMessage, $messageConstraint); - - /** @var CookieManagerInterface $cookieManager */ - $cookieManager = $this->_objectManager->get(CookieManagerInterface::class); - $cookieManager->deleteCookie(MessagePlugin::MESSAGES_COOKIES_NAME); - - $this->_objectManager->removeSharedInstance(Http::class); - $this->_objectManager->removeSharedInstance(Request::class); - $this->_request = null; - - $this->getRequest() - ->setParam('id', $customer->getId()) - ->setParam('key', $confirmation); - $this->dispatch('customer/account/confirm'); - - /** @var StoreManager $store */ - $store = $this->_objectManager->get(StoreManagerInterface::class); - $name = $store->getStore()->getFrontendName(); - - $this->assertRedirect($this->stringContains('customer/account/index/')); - $this->assertSessionMessages( - $this->equalTo(["Thank you for registering with {$name}."]), - MessageInterface::TYPE_SUCCESS - ); - $this->assertEmpty($customerRepository->get($email)->getConfirmation()); - } - /** * Test that confirmation email address displays special characters correctly. * @@ -867,74 +723,6 @@ protected function resetRequest(): void parent::resetRequest(); } - /** - * @param string $email - * @return void - */ - private function fillRequestWithAccountData($email) - { - $this->getRequest() - ->setMethod('POST') - ->setParam('firstname', 'firstname1') - ->setParam('lastname', 'lastname1') - ->setParam('company', '') - ->setParam('email', $email) - ->setParam('password', '_Password1') - ->setParam('password_confirmation', '_Password1') - ->setParam('telephone', '5123334444') - ->setParam('street', ['1234 fake street', '']) - ->setParam('city', 'Austin') - ->setParam('region_id', 57) - ->setParam('region', '') - ->setParam('postcode', '78701') - ->setParam('country_id', 'US') - ->setParam('default_billing', '1') - ->setParam('default_shipping', '1') - ->setParam('is_subscribed', '0') - ->setPostValue('create_address', true); - } - - /** - * @param string $email - * @return void - */ - private function fillRequestWithAccountDataAndFormKey($email) - { - $this->fillRequestWithAccountData($email); - $formKey = $this->_objectManager->get(FormKey::class); - $this->getRequest()->setParam('form_key', $formKey->getFormKey()); - } - - /** - * Returns stored customer by email. - * - * @param string $email - * @return CustomerInterface - */ - private function getCustomerByEmail($email) - { - /** @var FilterBuilder $filterBuilder */ - $filterBuilder = $this->_objectManager->get(FilterBuilder::class); - $filters = [ - $filterBuilder->setField(CustomerInterface::EMAIL) - ->setValue($email) - ->create() - ]; - - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilters($filters) - ->create(); - - $customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); - $customers = $customerRepository->getList($searchCriteria) - ->getItems(); - - $customer = array_pop($customers); - - return $customer; - } - /** * Add new request info (request uri, path info, action name). * diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php index e12068ef62b21..bd2c26e449d72 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountTest.php @@ -13,9 +13,12 @@ use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Customer\Model\Customer; use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\EmailNotification; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory as TemplateCollectionFactory; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Api\SimpleDataObjectConverter; +use Magento\Framework\App\Config\MutableScopeConfigInterface; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; @@ -23,6 +26,7 @@ use Magento\Framework\Math\Random; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Validator\Exception; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; @@ -101,6 +105,16 @@ class CreateAccountTest extends TestCase */ private $encryptor; + /** + * @var MutableScopeConfigInterface + */ + private $mutableScopeConfig; + + /** + * @var TemplateCollectionFactory + */ + private $templateCollectionFactory; + /** * @inheritdoc */ @@ -117,9 +131,20 @@ protected function setUp(): void $this->customerModelFactory = $this->objectManager->get(CustomerFactory::class); $this->random = $this->objectManager->get(Random::class); $this->encryptor = $this->objectManager->get(EncryptorInterface::class); + $this->mutableScopeConfig = $this->objectManager->get(MutableScopeConfigInterface::class); + $this->templateCollectionFactory = $this->objectManager->get(TemplateCollectionFactory::class); parent::setUp(); } + /** + * @inheritdoc + */ + protected function tearDown(): void + { + parent::tearDown(); + $this->mutableScopeConfig->clean(); + } + /** * @dataProvider createInvalidAccountDataProvider * @param array $customerData @@ -220,6 +245,98 @@ public function createInvalidAccountDataProvider(): array ]; } + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer_welcome_email_template.php + * @return void + */ + public function testCreateAccountWithConfiguredWelcomeEmail(): void + { + $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_template'); + $this->setConfig([EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE => $emailTemplate,]); + $this->accountManagement->createAccount( + $this->populateCustomerEntity($this->defaultCustomerData), + '_Password1' + ); + $this->assertEmailData( + [ + 'name' => 'Owner', + 'email' => 'owner@example.com', + 'message' => 'Customer create account email template', + ] + ); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer_welcome_no_password_email_template.php + * @magentoConfigFixture current_store customer/create_account/email_identity support + * @return void + */ + public function testCreateAccountWithConfiguredWelcomeNoPasswordEmail(): void + { + $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_no_password_template'); + $this->setConfig([EmailNotification::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE => $emailTemplate,]); + $this->accountManagement->createAccount($this->populateCustomerEntity($this->defaultCustomerData)); + $this->assertEmailData( + [ + 'name' => 'CustomerSupport', + 'email' => 'support@example.com', + 'message' => 'Customer create account email no password template', + ] + ); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer_confirmation_email_template.php + * @magentoConfigFixture current_website customer/create_account/confirm 1 + * @magentoConfigFixture current_store customer/create_account/email_identity custom1 + * @return void + */ + public function testCreateAccountWithConfiguredConfirmationEmail(): void + { + $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_confirmation_template'); + $this->setConfig([EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE => $emailTemplate,]); + $this->accountManagement->createAccount( + $this->populateCustomerEntity($this->defaultCustomerData), + '_Password1' + ); + $this->assertEmailData( + [ + 'name' => 'Custom 1', + 'email' => 'custom1@example.com', + 'message' => 'Customer create account email confirmation template', + ] + ); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer_confirmed_email_template.php + * @magentoConfigFixture current_store customer/create_account/email_identity custom1 + * @magentoConfigFixture current_website customer/create_account/confirm 1 + * @return void + */ + public function testCreateAccountWithConfiguredConfirmedEmail(): void + { + $emailTemplate = $this->getCustomTemplateId('customer_create_account_email_confirmed_template'); + $this->setConfig([EmailNotification::XML_PATH_CONFIRMED_EMAIL_TEMPLATE => $emailTemplate,]); + $this->accountManagement->createAccount( + $this->populateCustomerEntity($this->defaultCustomerData), + '_Password1' + ); + $customer = $this->customerRepository->get('customer@example.com'); + $this->accountManagement->activate($customer->getEmail(), $customer->getConfirmation()); + $this->assertEmailData( + [ + 'name' => 'Custom 1', + 'email' => 'custom1@example.com', + 'message' => 'Customer create account email confirmed template', + ] + ); + } + /** * Assert that when you create customer account via admin, link with "set password" is send to customer email. * @@ -589,4 +706,53 @@ private function assertCustomerData( ); } } + + /** + * Sets config data. + * + * @param array $configs + * @return void + */ + private function setConfig(array $configs): void + { + foreach ($configs as $path => $value) { + $this->mutableScopeConfig->setValue($path, $value, ScopeInterface::SCOPE_STORE, 'default'); + } + } + + /** + * Assert email data. + * + * @param array $expectedData + * @return void + */ + private function assertEmailData(array $expectedData): void + { + $message = $this->transportBuilderMock->getSentMessage(); + $this->assertNotNull($message); + $messageFrom = $message->getFrom(); + $this->assertNotNull($messageFrom); + $messageFrom = reset($messageFrom); + $this->assertEquals($expectedData['name'], $messageFrom->getName()); + $this->assertEquals($expectedData['email'], $messageFrom->getEmail()); + $this->assertStringContainsString( + $expectedData['message'], + $message->getBody()->getParts()[0]->getRawContent(), + 'Expected message wasn\'t found in email content.' + ); + } + + /** + * Returns email template id by template code. + * + * @param string $templateCode + * @return int + */ + private function getCustomTemplateId(string $templateCode): int + { + return (int)$this->templateCollectionFactory->create() + ->addFieldToFilter('template_code', $templateCode) + ->getFirstItem() + ->getId(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php index ac55f93bc9e4b..eb638eeb329aa 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php @@ -14,11 +14,16 @@ use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\CustomerRegistry; use Magento\Customer\Model\ResourceModel\Address; +use Magento\Customer\Model\Vat; +use Magento\Customer\Observer\AfterAddressSaveObserver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObjectFactory; use Magento\Framework\Exception\InputException; use Magento\TestFramework\Directory\Model\GetRegionIdByName; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface as PsrLogger; /** * Assert that address was created as expected or address create throws expected error. @@ -88,6 +93,11 @@ class CreateAddressTest extends TestCase */ private $createdAddressesIds = []; + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + /** * @inheritdoc */ @@ -101,6 +111,7 @@ protected function setUp(): void $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); $this->addressRegistry = $this->objectManager->get(AddressRegistry::class); $this->addressResource = $this->objectManager->get(Address::class); + $this->dataObjectFactory = $this->objectManager->get(DataObjectFactory::class); parent::setUp(); } @@ -112,6 +123,7 @@ protected function tearDown(): void foreach ($this->createdAddressesIds as $createdAddressesId) { $this->addressRegistry->remove($createdAddressesId); } + $this->objectManager->removeSharedInstance(AfterAddressSaveObserver::class); parent::tearDown(); } @@ -326,6 +338,92 @@ public function createWrongAddressesDataProvider(): array ]; } + /** + * Assert that after address creation customer group is Group for Valid VAT ID - Domestic. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoConfigFixture current_store general/store_information/country_id AT + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_domestic_group 2 + * @return void + */ + public function testAddressCreatedWithGroupAssignByDomesticVatId(): void + { + $this->createVatMock(true, true); + $addressData = array_merge( + self::STATIC_CUSTOMER_ADDRESS_DATA, + [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT'] + ); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->createAddress((int)$customer->getId(), $addressData, false, true); + $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); + } + + /** + * Assert that after address creation customer group is Group for Valid VAT ID - Intra-Union. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoConfigFixture current_store general/store_information/country_id GR + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_intra_union_group 2 + * @return void + */ + public function testAddressCreatedWithGroupAssignByIntraUnionVatId(): void + { + $this->createVatMock(true, true); + $addressData = array_merge( + self::STATIC_CUSTOMER_ADDRESS_DATA, + [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT'] + ); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->createAddress((int)$customer->getId(), $addressData, false, true); + $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); + } + + /** + * Assert that after address creation customer group is Group for Invalid VAT ID. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_invalid_group 2 + * @return void + */ + public function testAddressCreatedWithGroupAssignByInvalidVatId(): void + { + $this->createVatMock(false, true); + $addressData = array_merge( + self::STATIC_CUSTOMER_ADDRESS_DATA, + [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT'] + ); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->createAddress((int)$customer->getId(), $addressData, false, true); + $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); + } + + /** + * Assert that after address creation customer group is Validation Error Group. + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_error_group 2 + * @return void + */ + public function testAddressCreatedWithGroupAssignByVatIdWithError(): void + { + $this->createVatMock(false, false); + $addressData = array_merge( + self::STATIC_CUSTOMER_ADDRESS_DATA, + [AddressInterface::VAT_ID => '111', AddressInterface::COUNTRY_ID => 'AT'] + ); + $customer = $this->customerRepository->get('customer5@example.com'); + $this->createAddress((int)$customer->getId(), $addressData, false, true); + $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); + } + /** * Create customer address with provided address data. * @@ -361,4 +459,49 @@ protected function createAddress( return $address; } + + /** + * Creates mock for vat id validation. + * + * @param bool $isValid + * @param bool $isRequestSuccess + * @return void + */ + private function createVatMock(bool $isValid = false, bool $isRequestSuccess = false): void + { + $gatewayResponse = $this->dataObjectFactory->create( + [ + 'data' => [ + 'is_valid' => $isValid, + 'request_date' => '', + 'request_identifier' => '123123123', + 'request_success' => $isRequestSuccess, + 'request_message' => __(''), + ], + ] + ); + $customerVat = $this->getMockBuilder(Vat::class) + ->setConstructorArgs( + [ + $this->objectManager->get(ScopeConfigInterface::class), + $this->objectManager->get(PsrLogger::class) + ] + ) + ->setMethods(['checkVatNumber']) + ->getMock(); + $customerVat->method('checkVatNumber')->willReturn($gatewayResponse); + $this->objectManager->removeSharedInstance(Vat::class); + $this->objectManager->addSharedInstance($customerVat, Vat::class); + } + + /** + * Returns customer group id by email. + * + * @param string $email + * @return int + */ + private function getCustomerGroupId(string $email): int + { + return (int)$this->customerRepository->get($email)->getGroupId(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByTokenTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByTokenTest.php new file mode 100644 index 0000000000000..55e9a9572d229 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByTokenTest.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\ForgotPasswordToken; + +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class GetCustomerByTokenTest extends TestCase +{ + private const RESET_PASSWORD = '8ed8677e6c79e68b94e61658bd756ea5'; + + /** @var ObjectManagerInterface */ + private $objectManager; + + /** + * @var GetCustomerByToken + */ + private $customerByToken; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerByToken = $this->objectManager->get(GetCustomerByToken::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testExecuteWithNoSuchEntityException(): void + { + self::expectException(NoSuchEntityException::class); + self::expectExceptionMessage('No such entity with rp_token = ' . self::RESET_PASSWORD); + $this->customerByToken->execute(self::RESET_PASSWORD); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php new file mode 100644 index 0000000000000..38b607230cbaf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TemplateInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var TemplateInterfaceFactory $templateFactory */ +$templateFactory = $objectManager->get(TemplateInterfaceFactory::class); +/** @var TemplateInterface $template */ +$template = $templateFactory->create(); + +$content = <<<HTML +{{template config_path="design/email/header_template"}} +<p>{{trans "Customer create account email confirmation template"}}</p> +{{template config_path="design/email/footer_template"}} +HTML; + +$template->setTemplateCode('customer_create_account_email_confirmation_template') + ->setTemplateText($content) + ->setTemplateType(TemplateInterface::TYPE_HTML); +$templateResource->save($template); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_rollback.php new file mode 100644 index 0000000000000..07fee6e81fe47 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmation_email_template_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\ResourceModel\Template\Collection; +use Magento\Framework\Mail\TemplateInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var Collection $collection */ +$collection = $objectManager->get(CollectionFactory::class)->create(); +/** @var TemplateInterface $template */ +$template = $collection + ->addFieldToFilter('template_code', 'customer_create_account_email_confirmation_template') + ->getFirstItem(); +if ($template->getId()) { + $templateResource->delete($template); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php new file mode 100644 index 0000000000000..859cae92dbd27 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TemplateInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var TemplateInterfaceFactory $templateFactory */ +$templateFactory = $objectManager->get(TemplateInterfaceFactory::class); +/** @var TemplateInterface $template */ +$template = $templateFactory->create(); + +$content = <<<HTML +{{template config_path="design/email/header_template"}} +<p>{{trans "Customer create account email confirmed template"}}</p> +{{template config_path="design/email/footer_template"}} +HTML; + +$template->setTemplateCode('customer_create_account_email_confirmed_template') + ->setTemplateText($content) + ->setTemplateType(TemplateInterface::TYPE_HTML); +$templateResource->save($template); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_rollback.php new file mode 100644 index 0000000000000..a4e03038d45bd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_confirmed_email_template_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\ResourceModel\Template\Collection; +use Magento\Framework\Mail\TemplateInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var Collection $collection */ +$collection = $objectManager->get(CollectionFactory::class)->create(); +/** @var TemplateInterface $template */ +$template = $collection + ->addFieldToFilter('template_code', 'customer_create_account_email_confirmed_template') + ->getFirstItem(); +if ($template->getId()) { + $templateResource->delete($template); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php new file mode 100644 index 0000000000000..6cc273dbe235a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TemplateInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var TemplateInterfaceFactory $templateFactory */ +$templateFactory = $objectManager->get(TemplateInterfaceFactory::class); +/** @var TemplateInterface $template */ +$template = $templateFactory->create(); + +$content = <<<HTML +{{template config_path="design/email/header_template"}} +<p>{{trans "Customer create account email template"}}</p> +{{template config_path="design/email/footer_template"}} +HTML; + +$template->setTemplateCode('customer_create_account_email_template') + ->setTemplateText($content) + ->setTemplateType(TemplateInterface::TYPE_HTML); +$templateResource->save($template); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_rollback.php new file mode 100644 index 0000000000000..6bef9822d3e9a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_email_template_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\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\ResourceModel\Template\Collection; +use Magento\Framework\Mail\TemplateInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var Collection $collection */ +$collection = $objectManager->get(CollectionFactory::class)->create(); +/** @var TemplateInterface $template */ +$template = $collection->addFieldToFilter('template_code', 'customer_create_account_email_template')->getFirstItem(); +if ($template->getId()) { + $templateResource->delete($template); +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php new file mode 100644 index 0000000000000..a936bb9a4eb02 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Framework\Mail\TemplateInterface; +use Magento\Framework\Mail\TemplateInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var TemplateInterfaceFactory $templateFactory */ +$templateFactory = $objectManager->get(TemplateInterfaceFactory::class); +/** @var TemplateInterface $template */ +$template = $templateFactory->create(); + +$content = <<<HTML +{{template config_path="design/email/header_template"}} +<p>{{trans "Customer create account email no password template"}}</p> +{{template config_path="design/email/footer_template"}} +HTML; + +$template->setTemplateCode('customer_create_account_email_no_password_template') + ->setTemplateText($content) + ->setTemplateType(TemplateInterface::TYPE_HTML); +$templateResource->save($template); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_rollback.php new file mode 100644 index 0000000000000..4e14b4293cbb5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_welcome_no_password_email_template_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Email\Model\ResourceModel\Template as TemplateResource; +use Magento\Email\Model\ResourceModel\Template\CollectionFactory; +use Magento\Email\Model\ResourceModel\Template\Collection; +use Magento\Framework\Mail\TemplateInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TemplateResource $templateResource */ +$templateResource = $objectManager->get(TemplateResource::class); +/** @var Collection $collection */ +$collection = $objectManager->get(CollectionFactory::class)->create(); +/** @var TemplateInterface $template */ +$template = $collection + ->addFieldToFilter('template_code', 'customer_create_account_email_no_password_template') + ->getFirstItem(); +if ($template->getId()) { + $templateResource->delete($template); +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer_rollback.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer_rollback.php index 9ca4f78660b3a..a97faa29a1588 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/indexer_rollback.php @@ -4,6 +4,10 @@ * See COPYING.txt for license details. */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute_rollback.php'); + /** @var $objectManager \Magento\Framework\ObjectManagerInterface */ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); diff --git a/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/Model/SynchronizeFilesTest.php b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/Model/SynchronizeFilesTest.php new file mode 100644 index 0000000000000..52e7191a97226 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/Model/SynchronizeFilesTest.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationMetadata\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for SynchronizeFiles. + */ +class SynchronizeFilesTest extends TestCase +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPath; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->synchronizeFiles = Bootstrap::getObjectManager()->get(SynchronizeFilesInterface::class); + $this->getAssetsByPath = Bootstrap::getObjectManager()->get(GetAssetsByPathsInterface::class); + $this->getAssetKeywords = Bootstrap::getObjectManager()->get(GetAssetsKeywordsInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + } + + /** + * Test for SynchronizeFiles::execute + * + * @dataProvider filesProvider + * @param null|string $file + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @throws FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testExecute( + ?string $file, + ?string $title, + ?string $description, + ?array $keywords + ): void { + $path = realpath(__DIR__ . '/../_files/' . $file); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($file); + $this->driver->copy( + $path, + $modifiableFilePath + ); + + $this->synchronizeFiles->execute([$file]); + + $loadedAssets = $this->getAssetsByPath->execute([$file])[0]; + $loadedKeywords = $this->getKeywords($loadedAssets) ?: null; + + $this->assertEquals($title, $loadedAssets->getTitle()); + $this->assertEquals($description, $loadedAssets->getDescription()); + $this->assertEquals($keywords, $loadedKeywords); + + $this->driver->deleteFile($modifiableFilePath); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + '/magento.jpg', + 'magento', + null, + null + ], + [ + '/magento_metadata.jpg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ] + ]; + } + + /** + * Key asset keywords + * + * @param AssetInterface $asset + * @return string[] + */ + private function getKeywords(AssetInterface $asset): array + { + $assetKeywords = $this->getAssetKeywords->execute([$asset->getId()]); + + if (empty($assetKeywords)) { + return []; + } + + $keywords = current($assetKeywords)->getKeywords(); + + return array_map( + function (KeywordInterface $keyword) { + return $keyword->getKeyword(); + }, + $keywords + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento.jpg b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento.jpg new file mode 100644 index 0000000000000..c377daf8fb0b3 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento.jpg differ diff --git a/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento_metadata.jpg b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento_metadata.jpg new file mode 100644 index 0000000000000..6dc8cd69e41c1 Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento_metadata.jpg differ diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php index 370dc552458b9..dbf8bce795548 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/SubscriberTest.php @@ -8,6 +8,7 @@ namespace Magento\Newsletter\Model; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Mail\EmailMessage; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface; @@ -22,13 +23,16 @@ */ class SubscriberTest extends TestCase { - /** @var ObjectManagerInterface */ + private const CONFIRMATION_SUBSCRIBE = 'You have been successfully subscribed to our newsletter.'; + private const CONFIRMATION_UNSUBSCRIBE = 'You have been unsubscribed from the newsletter.'; + + /** @var ObjectManagerInterface */ private $objectManager; /** @var SubscriberFactory */ private $subscriberFactory; - /** @var TransportBuilderMock */ + /** @var TransportBuilderMock */ private $transportBuilder; /** @var CustomerRepositoryInterface */ @@ -89,27 +93,20 @@ public function testUnsubscribeSubscribe(): void $subscriber = $this->subscriberFactory->create(); $this->assertSame($subscriber, $subscriber->loadByCustomerId(1)); $this->assertEquals($subscriber, $subscriber->unsubscribe()); - $this->assertStringContainsString( - 'You have been unsubscribed from the newsletter.', - $this->getFilteredRawMessage($this->transportBuilder) + $this->assertConfirmationParagraphExists( + self::CONFIRMATION_UNSUBSCRIBE, + $this->transportBuilder->getSentMessage() ); + $this->assertEquals(Subscriber::STATUS_UNSUBSCRIBED, $subscriber->getSubscriberStatus()); // Subscribe and verify $this->assertEquals(Subscriber::STATUS_SUBSCRIBED, $subscriber->subscribe('customer@example.com')); $this->assertEquals(Subscriber::STATUS_SUBSCRIBED, $subscriber->getSubscriberStatus()); - $this->assertStringContainsString( - 'You have been successfully subscribed to our newsletter.', - $this->getFilteredRawMessage($this->transportBuilder) - ); - } - /** - * @param TransportBuilderMock $transportBuilderMock - * @return string - */ - private function getFilteredRawMessage(TransportBuilderMock $transportBuilderMock): string - { - return $transportBuilderMock->getSentMessage()->getBody()->getParts()[0]->getRawContent(); + $this->assertConfirmationParagraphExists( + self::CONFIRMATION_SUBSCRIBE, + $this->transportBuilder->getSentMessage() + ); } /** @@ -125,16 +122,17 @@ public function testUnsubscribeSubscribeByCustomerId(): void // Unsubscribe and verify $this->assertSame($subscriber, $subscriber->unsubscribeCustomerById(1)); $this->assertEquals(Subscriber::STATUS_UNSUBSCRIBED, $subscriber->getSubscriberStatus()); - $this->assertStringContainsString( - 'You have been unsubscribed from the newsletter.', - $this->getFilteredRawMessage($this->transportBuilder) + $this->assertConfirmationParagraphExists( + self::CONFIRMATION_UNSUBSCRIBE, + $this->transportBuilder->getSentMessage() ); + // Subscribe and verify $this->assertSame($subscriber, $subscriber->subscribeCustomerById(1)); $this->assertEquals(Subscriber::STATUS_SUBSCRIBED, $subscriber->getSubscriberStatus()); - $this->assertStringContainsString( - 'You have been successfully subscribed to our newsletter.', - $this->getFilteredRawMessage($this->transportBuilder) + $this->assertConfirmationParagraphExists( + self::CONFIRMATION_SUBSCRIBE, + $this->transportBuilder->getSentMessage() ); } @@ -152,9 +150,10 @@ public function testConfirm(): void $subscriber->subscribe($customerEmail); $subscriber->loadByEmail($customerEmail); $subscriber->confirm($subscriber->getSubscriberConfirmCode()); - $this->assertStringContainsString( - 'You have been successfully subscribed to our newsletter.', - $this->getFilteredRawMessage($this->transportBuilder) + + $this->assertConfirmationParagraphExists( + self::CONFIRMATION_SUBSCRIBE, + $this->transportBuilder->getSentMessage() ); } @@ -189,4 +188,35 @@ public function testSubscribeUnconfirmedCustomerWithoutSubscription(): void $subscriber->subscribeCustomerById($customer->getId()); $this->assertEquals(Subscriber::STATUS_UNCONFIRMED, $subscriber->getStatus()); } + + /** + * Verifies if Paragraph with specified message is in e-mail + * + * @param string $expectedMessage + * @param EmailMessage $message + */ + private function assertConfirmationParagraphExists(string $expectedMessage, EmailMessage $message): void + { + $messageContent = $this->getMessageRawContent($message); + + $emailDom = new \DOMDocument(); + $emailDom->loadHTML($messageContent); + + $emailXpath = new \DOMXPath($emailDom); + $greeting = $emailXpath->query("//p[contains(text(), '$expectedMessage')]"); + + $this->assertSame(1, $greeting->length, "Cannot find the confirmation paragraph in e-mail contents"); + } + + /** + * Returns raw content of provided message + * + * @param EmailMessage $message + * @return string + */ + private function getMessageRawContent(EmailMessage $message): string + { + $emailParts = $message->getBody()->getParts(); + return current($emailParts)->getRawContent(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php new file mode 100644 index 0000000000000..ca1f309c5cc9b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Block/Form/RememberTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Block\Form; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Test for remember me checkbox on create customer account page + * + * @see \Magento\Persistent\Block\Form\Remember + * @magentoAppArea frontend + */ +class RememberTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Remember */ + private $block; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Remember::class) + ->setTemplate('Magento_Persistent::remember_me.phtml'); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_default 0 + * + * @return void + */ + public function testRememberMeEnabled(): void + { + $this->assertFalse($this->block->isRememberMeChecked()); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + '//input[@name="persistent_remember_me"]/following-sibling::label/span[contains(text(), "%s")]', + __('Remember Me') + ), + $this->block->toHtml() + ), + 'Remember Me checkbox wasn\'t found.' + ); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_default 1 + * + * @return void + */ + public function testRememberMeAndRememberDefaultEnabled(): void + { + $this->assertTrue($this->block->isRememberMeChecked()); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + '//input[@name="persistent_remember_me"]/following-sibling::label/span[contains(text(), "%s")]', + __('Remember Me') + ), + $this->block->toHtml() + ), + 'Remember Me checkbox wasn\'t found or not checked by default.' + ); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 0 + * + * @return void + */ + public function testPersistentDisabled(): void + { + $this->assertEmpty($this->block->toHtml()); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 0 + * + * @return void + */ + public function testRememberMeDisabled(): void + { + $this->assertEmpty($this->block->toHtml()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php new file mode 100644 index 0000000000000..16ce015d89ecd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Helper/SessionTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Helper; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for persistent session helper + * + * @see \Magento\Persistent\Helper\Session + * @magentoDbIsolation enabled + * @magentoAppArea frontend + */ +class SessionTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Session */ + private $helper; + + /** @var SessionFactory */ + private $sessionFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(Session::class); + $this->sessionFactory = $this->objectManager->get(SessionFactory::class); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testPersistentEnabled(): void + { + $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1)); + $this->assertTrue($this->helper->isPersistent()); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent.php + * @magentoConfigFixture current_store persistent/options/enabled 0 + * + * @return void + */ + public function testPersistentDisabled(): void + { + $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1)); + $this->assertFalse($this->helper->isPersistent()); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testCustomerWithoutPersistent(): void + { + $this->helper->setSession($this->sessionFactory->create()->loadByCustomerId(1)); + $this->assertFalse($this->helper->isPersistent()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php new file mode 100644 index 0000000000000..803e1502e3ad9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/Checkout/ConfigProviderPluginTest.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model\Checkout; + +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Checkout\Model\DefaultConfigProvider; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Helper\Session as PersistentSessionHelper; +use Magento\Persistent\Model\Session as PersistentSession; +use Magento\Persistent\Model\SessionFactory as PersistentSessionFactory; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Interception\PluginList; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Test for checkout config provider plugin + * + * @see \Magento\Persistent\Model\Checkout\ConfigProviderPlugin + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ConfigProviderPluginTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var DefaultConfigProvider */ + private $configProvider; + + /** @var CustomerSession */ + private $customerSession; + + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var QuoteIdMask */ + private $quoteIdMask; + + /** @var PersistentSessionHelper */ + private $persistentSessionHelper; + + /** @var PersistentSession */ + private $persistentSession; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->configProvider = $this->objectManager->get(DefaultConfigProvider::class); + $this->customerSession = $this->objectManager->get(CustomerSession::class); + $this->checkoutSession = $this->objectManager->get(CheckoutSession::class); + $this->quoteIdMask = $this->objectManager->get(QuoteIdMaskFactory::class)->create(); + $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class); + $this->persistentSession = $this->objectManager->get(PersistentSessionFactory::class)->create(); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->customerSession->setCustomerId(null); + $this->checkoutSession->clearQuote(); + $this->checkoutSession->setCustomerData(null); + $this->persistentSessionHelper->setSession(null); + + parent::tearDown(); + } + + /** + * @return void + */ + public function testPluginIsRegistered(): void + { + $pluginInfo = $this->objectManager->get(PluginList::class)->get(DefaultConfigProvider::class); + $this->assertSame(ConfigProviderPlugin::class, $pluginInfo['mask_quote_id_substitutor']['instance']); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testWithNotLoggedCustomer(): void + { + $session = $this->persistentSession->loadByCustomerId(1); + $this->persistentSessionHelper->setSession($session); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $result = $this->configProvider->getConfig(); + $this->assertEquals( + $this->quoteIdMask->load($quote->getId(), 'quote_id')->getMaskedId(), + $result['quoteData']['entity_id'] + ); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testWithLoggedCustomer(): void + { + $this->customerSession->setCustomerId(1); + $session = $this->persistentSession->loadByCustomerId(1); + $this->persistentSessionHelper->setSession($session); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $result = $this->configProvider->getConfig(); + $this->assertEquals($quote->getId(), $result['quoteData']['entity_id']); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * @magentoConfigFixture current_store persistent/options/enabled 0 + * + * @return void + */ + public function testPersistentDisabled(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $result = $this->configProvider->getConfig(); + $this->assertNull($result['quoteData']['entity_id']); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * + * @return void + */ + public function testWithoutPersistentSession(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $result = $this->configProvider->getConfig(); + $this->assertNull($result['quoteData']['entity_id']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php new file mode 100644 index 0000000000000..176224bad7a1f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/CheckoutConfigProviderTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for remember me checkbox on create customer account page. + * + * @see \Magento\Persistent\Model\CheckoutConfigProvider + */ +class CheckoutConfigProviderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CheckoutConfigProvider */ + private $model; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(CheckoutConfigProvider::class); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_default 1 + * + * @return void + */ + public function testRememberMeEnabled(): void + { + $expectedConfig = [ + 'persistenceConfig' => ['isRememberMeCheckboxVisible' => true, 'isRememberMeCheckboxChecked' => true], + ]; + $config = $this->model->getConfig(); + $this->assertEquals($expectedConfig, $config); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/remember_enabled 0 + * @magentoConfigFixture current_store persistent/options/remember_default 0 + * + * @return void + */ + public function testRememberMeDisabled(): void + { + $expectedConfig = [ + 'persistenceConfig' => ['isRememberMeCheckboxVisible' => false, 'isRememberMeCheckboxChecked' => false], + ]; + $config = $this->model->getConfig(); + $this->assertEquals($expectedConfig, $config); + } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 0 + * @magentoConfigFixture current_store persistent/options/remember_default 0 + * + * @return void + */ + public function testPersistentDisabled(): void + { + $expectedConfig = [ + 'persistenceConfig' => ['isRememberMeCheckboxVisible' => false, 'isRememberMeCheckboxChecked' => false], + ]; + $config = $this->model->getConfig(); + $this->assertEquals($expectedConfig, $config); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php new file mode 100644 index 0000000000000..e11d47af3e814 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/Model/QuoteManagerTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Persistent\Model; + +use Magento\Customer\Api\Data\GroupInterface; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Helper\Session as PersistentSessionHelper; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Test for persistent quote manager model + * + * @see \Magento\Persistent\Model\QuoteManager + * @magentoDbIsolation enabled + */ +class QuoteManagerTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var QuoteManager */ + private $model; + + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** @var PersistentSessionHelper */ + private $persistentSessionHelper; + + /** @var CartInterface */ + private $quote; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(QuoteManager::class); + $this->checkoutSession = $this->objectManager->get(CheckoutSession::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->checkoutSession->clearQuote(); + $this->checkoutSession->setCustomerData(null); + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/shopping_cart 1 + * + * @return void + */ + public function testPersistentShoppingCartEnabled(): void + { + $customerQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($customerQuote->getId()); + $this->model->setGuest(true); + $this->quote = $this->checkoutSession->getQuote(); + $this->assertNotEquals($customerQuote->getId(), $this->quote->getId()); + $this->assertFalse($this->model->isPersistent()); + $this->assertNull($this->quote->getCustomerId()); + $this->assertNull($this->quote->getCustomerEmail()); + $this->assertNull($this->quote->getCustomerFirstname()); + $this->assertNull($this->quote->getCustomerLastname()); + $this->assertEquals(GroupInterface::NOT_LOGGED_IN_ID, $this->quote->getCustomerGroupId()); + $this->assertEmpty($this->quote->getIsPersistent()); + $this->assertNull($this->persistentSessionHelper->getSession()->getId()); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/shopping_cart 0 + * + * @return void + */ + public function testPersistentShoppingCartDisabled(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($quote->getId()); + $this->model->setGuest(true); + $this->assertNull($this->checkoutSession->getQuote()->getId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php index 35f2283494b1c..bd4d24211f1e3 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php @@ -10,16 +10,21 @@ use DateTime; use DateTimeZone; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Framework\Event; -use Magento\Framework\Event\Observer; +use Magento\Customer\Model\Session as CustomerSession; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Persistent\Helper\Session as PersistentSessionHelper; use Magento\Persistent\Model\Session; use Magento\Persistent\Model\SessionFactory; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** + * Test for synchronize persistent session on login observer + * + * @see \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver + * @magentoAppArea frontend + * @magentoDbIsolation enabled * @magentoDataFixture Magento/Customer/_files/customer.php * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -28,87 +33,129 @@ class SynchronizePersistentOnLoginObserverTest extends TestCase /** * @var SynchronizePersistentOnLoginObserver */ - protected $_model; + private $model; /** * @var ObjectManagerInterface */ - protected $_objectManager; + private $objectManager; /** - * @var \Magento\Persistent\Helper\Session + * @var PersistentSessionHelper */ - protected $_persistentSession; + private $persistentSessionHelper; /** - * @var \Magento\Customer\Model\Session + * @var CustomerRepositoryInterface */ - protected $_customerSession; + private $customerRepository; /** - * @var CustomerInterface + * @var SessionFactory */ - private $customer; + private $persistentSessionFactory; + + /** + * @var CookieManagerInterface + */ + private $cookieManager; + + /** + * @var CustomerSession + */ + private $customerSession; /** * @inheritDoc */ protected function setUp(): void { - $this->_objectManager = Bootstrap::getObjectManager(); - $this->_persistentSession = $this->_objectManager->get(\Magento\Persistent\Helper\Session::class); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); - $this->_model = $this->_objectManager->create( - SynchronizePersistentOnLoginObserver::class, - [ - 'persistentSession' => $this->_persistentSession, - 'customerSession' => $this->_customerSession - ] - ); - /** @var CustomerRepositoryInterface $customerRepository */ - $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); - $this->customer = $customerRepository->getById(1); + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->persistentSessionHelper = $this->objectManager->get(PersistentSessionHelper::class); + $this->model = $this->objectManager->get(SynchronizePersistentOnLoginObserver::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->persistentSessionFactory = $this->objectManager->get(SessionFactory::class); + $this->cookieManager = $this->objectManager->get(CookieManagerInterface::class); + $this->customerSession = $this->objectManager->get(CustomerSession::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->persistentSessionHelper->setRememberMeChecked(null); + $this->customerSession->logout(); + + parent::tearDown(); } /** * Test that persistent session is created on customer login + * + * @return void */ public function testSynchronizePersistentOnLogin(): void { - $sessionModel = $this->_objectManager->create(Session::class); - $sessionModel->loadByCustomerId($this->customer->getId()); + $customer = $this->customerRepository->get('customer@example.com'); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->loadByCustomerId($customer->getId()); $this->assertNull($sessionModel->getCustomerId()); - $event = new Event(); - $observer = new Observer(['event' => $event]); - $event->setData('customer', $this->customer); - $this->_persistentSession->setRememberMeChecked(true); - $this->_model->execute($observer); - // check that persistent session has been stored for Customer - /** @var Session $sessionModel */ - $sessionModel = $this->_objectManager->create(Session::class); - $sessionModel->loadByCustomerId($this->customer->getId()); - $this->assertEquals($this->customer->getId(), $sessionModel->getCustomerId()); + $this->persistentSessionHelper->setRememberMeChecked(true); + $this->customerSession->loginById($customer->getId()); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->loadByCustomerId($customer->getId()); + $this->assertEquals($customer->getId(), $sessionModel->getCustomerId()); } /** * Test that expired persistent session is renewed on customer login + * + * @return void */ public function testExpiredPersistentSessionShouldBeRenewedOnLogin(): void { + $customer = $this->customerRepository->get('customer@example.com'); $lastUpdatedAt = (new DateTime('-1day'))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s'); - /** @var Session $sessionModel */ - $sessionModel = $this->_objectManager->create(SessionFactory::class)->create(); - $sessionModel->setCustomerId($this->customer->getId()); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->setCustomerId($customer->getId()); $sessionModel->setUpdatedAt($lastUpdatedAt); $sessionModel->save(); - $event = new Event(); - $observer = new Observer(['event' => $event]); - $event->setData('customer', $this->customer); - $this->_persistentSession->setRememberMeChecked(true); - $this->_model->execute($observer); - /** @var Session $sessionModel */ - $sessionModel = $this->_objectManager->create(Session::class); - $sessionModel->loadByCustomerId(1); + $this->persistentSessionHelper->setRememberMeChecked(true); + $this->customerSession->loginById($customer->getId()); + $sessionModel = $this->persistentSessionFactory->create(); + $sessionModel->loadByCustomerId($customer->getId()); $this->assertGreaterThan($lastUpdatedAt, $sessionModel->getUpdatedAt()); } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 0 + * + * @return void + */ + public function testDisabledPersistentSession(): void + { + $customer = $this->customerRepository->get('customer@example.com'); + $this->customerSession->loginById($customer->getId()); + $this->assertNull($this->cookieManager->getCookie(Session::COOKIE_NAME)); + } + + /** + * @magentoDataFixture Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/lifetime 0 + * + * @return void + */ + public function testDisabledPersistentSessionLifetime(): void + { + $customer = $this->customerRepository->get('customer@example.com'); + $this->customerSession->loginById($customer->getId()); + $session = $this->persistentSessionFactory->create()->setLoadExpired()->loadByCustomerId($customer->getId()); + $this->assertNull($session->getId()); + $this->assertNull($this->cookieManager->getCookie(Session::COOKIE_NAME)); + } } diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php index 2bf97fdb4953f..293f1d1890d92 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLogoutObserverTest.php @@ -3,54 +3,77 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Persistent\Observer; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** + * Test for synchronize persistent on logout observer + * + * @see \Magento\Persistent\Observer\SynchronizePersistentOnLogoutObserver * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoAppArea frontend + * @magentoDbIsolation enabled */ -class SynchronizePersistentOnLogoutObserverTest extends \PHPUnit\Framework\TestCase +class SynchronizePersistentOnLogoutObserverTest extends TestCase { - /** - * @var \Magento\Framework\ObjectManagerInterface - */ - protected $_objectManager; + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CustomerSession */ + private $customerSession; + + /** @var SessionFactory */ + private $sessionFactory; /** - * @var \Magento\Customer\Model\Session + * @inheritdoc */ - protected $_customerSession; - protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(CustomerSession::class); + $this->sessionFactory = $this->objectManager->get(SessionFactory::class); } /** * @magentoConfigFixture current_store persistent/options/enabled 1 * @magentoConfigFixture current_store persistent/options/logout_clear 1 - * @magentoAppArea frontend - * @magentoAppIsolation enabled + * + * @return void */ - public function testSynchronizePersistentOnLogout() + public function testSynchronizePersistentOnLogout(): void { - $this->_customerSession->loginById(1); - - // check that persistent session has been stored for Customer - /** @var \Magento\Persistent\Model\Session $sessionModel */ - $sessionModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Persistent\Model\Session::class - ); + $this->customerSession->loginById(1); + $sessionModel = $this->sessionFactory->create(); $sessionModel->loadByCookieKey(); $this->assertEquals(1, $sessionModel->getCustomerId()); - - $this->_customerSession->logout(); - - /** @var \Magento\Persistent\Model\Session $sessionModel */ - $sessionModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Persistent\Model\Session::class - ); + $this->customerSession->logout(); + $sessionModel = $this->sessionFactory->create(); $sessionModel->loadByCookieKey(); $this->assertNull($sessionModel->getCustomerId()); } + + /** + * @magentoConfigFixture current_store persistent/options/enabled 1 + * @magentoConfigFixture current_store persistent/options/logout_clear 0 + * + * @return void + */ + public function testSynchronizePersistentOnLogoutDisabled(): void + { + $this->customerSession->loginById(1); + $this->customerSession->logout(); + $sessionModel = $this->sessionFactory->create(); + $sessionModel->loadByCookieKey(); + $this->assertEquals(1, $sessionModel->getCustomerId()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_rollback.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_rollback.php new file mode 100644 index 0000000000000..581ddb35e3678 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php new file mode 100644 index 0000000000000..a2c68ad9b7f2a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_customer_without_address.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var SessionFactory $persistentSessionFactory */ +$persistentSessionFactory = $objectManager->get(SessionFactory::class); +$session = $persistentSessionFactory->create(); +$session->setCustomerId(1)->save(); +$session->setPersistentCookie(10000, ''); diff --git a/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php new file mode 100644 index 0000000000000..252b3f4be7079 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Persistent/_files/persistent_with_customer_quote_and_cookie_rollback.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var SessionFactory $sessionFactory */ +$sessionFactory = $objectManager->get(SessionFactory::class); +$sessionFactory->create()->deleteByCustomerId(1); + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_customer_without_address_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/_files/product_alert_with_store_rollback.php b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/product_alert_with_store_rollback.php new file mode 100644 index 0000000000000..59aa4bda3872a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/_files/product_alert_with_store_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\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_for_second_store_rollback.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/Catalog/_files/product_simple_out_of_stock_without_categories_rollback.php' +); + +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = Bootstrap::getObjectManager()->create(CustomerRegistry::class); +$customer = $customerRegistry->remove(1); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +try { + $product = $productRepository->deleteById('simple'); +} catch (\Exception $e) { + // product already removed +} +/** @var Magento\Store\Model\Store $store */ +$store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); +$store->load('fixture_second_store'); +if ($store->getId()) { + $store->delete(); +} +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index f16986a3f2422..391be01b17f45 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -133,6 +133,6 @@ public function testChangeQuoteCustomerGroupIdForCustomerWithEnabledAutomaticGro ); $this->model->execute($eventObserver); - $this->assertEquals(1, $quote->getCustomer()->getGroupId()); + $this->assertEquals(2, $quote->getCustomer()->getGroupId()); } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product_rollback.php index 677bfb32cd8e9..f3d31bee387f6 100644 --- a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product_rollback.php @@ -7,5 +7,5 @@ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/out_of_stock_product_with_category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/out_of_stock_product_with_category_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer_rollback.php index f3dedf0a35d96..5c5ad143ac77f 100644 --- a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer_rollback.php @@ -43,3 +43,6 @@ $session->logout(); $config->setValue('reports/options/enabled', $originalValue); } + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php new file mode 100644 index 0000000000000..df5f5f8336303 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Block/Account/LinkTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Block\Account; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks "My Product Reviews" link displaying in customer account dashboard + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + */ +class LinkTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Page */ + private $page; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->page = $this->objectManager->get(PageFactory::class)->create(); + } + + /** + * @return void + */ + public function testMyProductReviewsLink(): void + { + $this->preparePage(); + $block = $this->page->getLayout()->getBlock('customer-account-navigation-product-reviews-link'); + $this->assertNotFalse($block); + $html = $block->toHtml(); + $this->assertStringContainsString('/review/customer/', $html); + $this->assertEquals((string)__('My Product Reviews'), strip_tags($html)); + } + + /** + * @magentoConfigFixture current_store catalog/review/active 0 + * + * @return void + */ + public function testMyProductReviewsLinkDisabled(): void + { + $this->preparePage(); + $block = $this->page->getLayout()->getBlock('customer-account-navigation-product-reviews-link'); + $this->assertFalse($block); + } + + /** + * Prepare page before render + * + * @return void + */ + private function preparePage(): void + { + $this->page->addHandle([ + 'default', + 'customer_account', + ]); + $this->page->getLayout()->generateXml(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php new file mode 100644 index 0000000000000..24cb2fe76a6d4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ListCustomerTest.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Block\Customer; + +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Test for customer product reviews grid. + * + * @see \Magento\Review\Block\Customer\ListCustomer + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ListCustomerTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Session */ + private $customerSession; + + /** @var ListCustomer */ + private $block; + + /** @var CollectionFactory */ + private $collectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(Session::class); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(ListCustomer::class) + ->setTemplate('Magento_Review::customer/list.phtml'); + $this->collectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Review/_files/customer_review_with_rating.php + * + * @return void + */ + public function testCustomerProductReviewsGrid(): void + { + $this->customerSession->setCustomerId(1); + $review = $this->collectionFactory->create()->addCustomerFilter(1)->addReviewSummary()->getFirstItem(); + $this->assertNotNull($review->getReviewId()); + $blockHtml = $this->block->toHtml(); + $createdDate = $this->block->dateFormat($review->getReviewCreatedAt()); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//td[contains(@class, 'date') and contains(text(), '%s')]", $createdDate), + $blockHtml + ), + sprintf('Created date wasn\'t found or not equals to %s.', $createdDate) + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//td[contains(@class, 'item')]//a[contains(text(), '%s')]", $review->getName()), + $blockHtml + ), + 'Product name wasn\'t found.' + ); + $rating = $review->getSum() / $review->getCount(); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//td[contains(@class, 'summary')]//span[contains(text(), '%s%%')]", $rating), + $blockHtml + ), + sprintf('Rating wasn\'t found or not equals to %s%%.', $rating) + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//td[contains(@class, 'description') and contains(text(), '%s')]", $review->getDetail()), + $blockHtml + ), + 'Review description wasn\'t found.' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//td[contains(@class, 'actions')]//a[contains(@href, '%s')]/span[contains(text(), '%s')]", + $this->block->getReviewUrl($review), + __('See Details') + ), + $blockHtml + ), + sprintf('%s button wasn\'t found.', __('See Details')) + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testCustomerWithoutReviews(): void + { + $this->customerSession->setCustomerId(1); + $this->assertStringContainsString( + (string)__('You have submitted no reviews.'), + strip_tags($this->block->toHtml()) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php new file mode 100644 index 0000000000000..31a342ad8ac54 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/Block/Customer/ViewTest.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Block\Customer; + +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Test for displaying customer product review block. + * + * @see \Magento\Review\Block\Customer\View + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ViewTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Session */ + private $customerSession; + + /** @var CollectionFactory */ + private $collectionFactory; + + /** @var View */ + private $block; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerSession = $this->objectManager->get(Session::class); + $this->collectionFactory = $this->objectManager->get(CollectionFactory::class); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(View::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Review/_files/customer_review_with_rating.php + * + * @return void + */ + public function testCustomerProductReviewBlock(): void + { + $this->customerSession->setCustomerId(1); + $review = $this->collectionFactory->create()->addCustomerFilter(1)->getFirstItem(); + $this->assertNotNull($review->getReviewId()); + $blockHtml = $this->block->setReviewId($review->getReviewId())->toHtml(); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//div[contains(@class, 'product-info')]/h2[contains(text(), '%s')]", $review->getName()), + $blockHtml + ), + 'Product name wasn\'t found.' + ); + $ratings = $this->block->getRating(); + $this->assertCount(2, $ratings); + foreach ($ratings as $rating) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'rating-summary')]//span[contains(text(), '%s')]" + . "/../..//span[contains(text(), '%s%%')]", + $rating->getRatingCode(), + $rating->getPercent() + ), + $blockHtml + ), + sprintf('Rating %s was not found or not equals to %s.', $rating->getRatingCode(), $rating->getPercent()) + ); + } + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//div[contains(@class, 'review-title') and contains(text(), '%s')]", $review->getTitle()), + $blockHtml + ), + 'Review title wasn\'t found.' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf("//div[contains(@class, 'review-content') and contains(text(), '%s')]", $review->getDetail()), + $blockHtml + ), + 'Review description wasn\'t found.' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//div[contains(@class, 'review-date') and contains(text(), '%s')]/time[contains(text(), '%s')]", + __('Submitted on'), + $this->block->dateFormat($review->getCreatedAt()) + ), + $blockHtml + ), + 'Created date wasn\'t found.' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + sprintf( + "//a[contains(@href, '/review/customer/')]/span[contains(text(), '%s')]", + __('Back to My Reviews') + ), + $blockHtml + ), + sprintf('%s button wasn\'t found.', __('Back to My Reviews')) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php new file mode 100644 index 0000000000000..b26b71803848f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Items/GridTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create\Items; + +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Block\Adminhtml\Order\Create\Items; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Checks order items grid + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class GridTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var LayoutInterface */ + private $layout; + + /** @var Grid */ + private $block; + + /** @var Quote */ + private $session; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->session = $this->objectManager->get(Quote::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(Grid::class); + $this->layout->createBlock(Items::class)->setChild('items_grid', $this->block); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testGetItems(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->session->setQuoteId($quote->getId()); + $items = $this->block->getItems(); + $this->assertCount(1, $items); + $this->assertEquals('simple2', reset($items)->getSku()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php new file mode 100644 index 0000000000000..291fda6e2494f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/CartTest.php @@ -0,0 +1,80 @@ +<?php +/** + * 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\Backend\Model\Session\Quote; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Check sidebar shopping cart section block + * + * @see \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\Cart + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class CartTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Cart */ + private $block; + + /** @var Quote */ + private $session; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Cart::class); + $this->session = $this->objectManager->get(Quote::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->session->clearStorage(); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testGetItemCollection(): void + { + $this->session->setCustomerId(1); + $items = $this->block->getItemCollection(); + $this->assertCount(1, $items); + $this->assertEquals('simple2', reset($items)->getSku()); + } + + /** + * @return void + */ + public function testClearShoppingCartButton(): void + { + $confirmation = __('Are you sure you want to delete all items from shopping cart?'); + $button = $this->block->getChildBlock('empty_customer_cart_button'); + $this->assertEquals(sprintf("order.clearShoppingCart('%s')", $confirmation), $button->getOnclick()); + $this->assertEquals(__('Clear Shopping Cart'), $button->getLabel()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php new file mode 100644 index 0000000000000..1b5772cec66de --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/ItemsTest.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Invoice\Create; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Model\OrderFactory; +use Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks invoiced items grid appearance + * + * @see \Magento\Sales\Block\Adminhtml\Order\Invoice\Create\Items + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class ItemsTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Items */ + private $block; + + /** @var OrderFactory */ + private $orderFactory; + + /** @var Registry */ + private $registry; + + /** @var CollectionFactory */ + private $invoiceCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Items::class); + $this->orderFactory = $this->objectManager->get(OrderFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->invoiceCollectionFactory = $this->objectManager->get(CollectionFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_invoice'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/invoice.php + * + * @return void + */ + public function testGetUpdateButtonHtml(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $invoice = $this->invoiceCollectionFactory->create()->setOrderFilter($order)->setPageSize(1)->getFirstItem(); + $this->registry->unregister('current_invoice'); + $this->registry->register('current_invoice', $invoice); + $this->block->toHtml(); + $button = $this->block->getChildBlock('update_button'); + $this->assertEquals((string)__('Update Qty\'s'), (string)$button->getLabel()); + $this->assertStringContainsString( + sprintf('sales/index/updateQty/order_id/%u/', (int)$order->getEntityId()), + $button->getOnClick() + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php new file mode 100644 index 0000000000000..a78c221cb5f84 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/ViewTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order; + +use Magento\Backend\Model\Search\AuthorizationMock; +use Magento\Framework\Authorization; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Checks order create block + * + * @see \Magento\Sales\Block\Adminhtml\Order\View + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class ViewTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var LayoutInterface */ + private $layout; + + /** @var Registry */ + private $registry; + + /** @var OrderFactory */ + private $orderFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->addSharedInstance( + $this->objectManager->get(AuthorizationMock::class), + Authorization::class + ); + $this->registry = $this->objectManager->get(Registry::class); + $this->orderFactory = $this->objectManager->get(OrderFactory::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('sales_order'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testInvoiceButton(): void + { + $this->registerOrder('100000001'); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath( + '//button[@id=\'order_invoice\']', + $this->layout->createBlock(View::class)->getButtonsHtml() + ) + ); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php + * + * @return void + */ + public function testInvoiceButtonIsNotVisible(): void + { + $this->registerOrder('100000001'); + $this->assertEmpty( + Xpath::getElementsCountForXpath( + '//button[@id=\'order_invoice\']', + $this->layout->createBlock(View::class)->getButtonsHtml() + ) + ); + } + + /** + * Register order + * + * @param OrderInterface $order + * @return void + */ + private function registerOrder(string $orderIncrementId): void + { + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + $this->registry->unregister('sales_order'); + $this->registry->register('sales_order', $order); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php new file mode 100644 index 0000000000000..b6aa44bac1c4d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/LoadBlockTest.php @@ -0,0 +1,301 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Create; + +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Class checks create order load block controller. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\LoadBlock + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class LoadBlockTest extends AbstractBackendController +{ + /** @var LayoutInterface */ + private $layout; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** @var Quote */ + private $session; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var array */ + private $quoteIdsToRemove; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->layout = $this->_objectManager->get(LayoutInterface::class); + $this->getQuoteByReservedOrderId = $this->_objectManager->get(GetQuoteByReservedOrderId::class); + $this->session = $this->_objectManager->get(Quote::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->quoteIdsToRemove[] = $this->session->getQuote()->getId(); + foreach ($this->quoteIdsToRemove as $quoteId) { + try { + $this->quoteRepository->delete($this->quoteRepository->get($quoteId)); + } catch (NoSuchEntityException $e) { + //do nothing + } + } + + $this->session->clearStorage(); + + parent::tearDown(); + } + + /** + * @dataProvider responseFlagsProvider + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @param bool $asJson + * @param bool $asJsVarname + * @return void + */ + public function testAddProductToOrderFromShoppingCart(bool $asJson, bool $asJsVarname): void + { + $oldQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $params = $this->hydrateParams([ + 'json' => $asJson, + 'as_js_varname' => $asJsVarname, + ]); + $post = $this->hydratePost([ + 'sidebar' => [ + 'add_cart_item' => [ + $oldQuote->getItemsCollection()->getFirstItem()->getId() => 1, + ], + ], + ]); + + $this->dispatchWitParams($params, $post); + + $this->checkHandles(explode(',', $params['block']), $asJson); + $this->checkQuotes($oldQuote, 'simple2'); + + if ($asJsVarname) { + $this->assertRedirect($this->stringContains('sales/order_create/showUpdateResult')); + } + } + + /** + * @return array + */ + public function responseFlagsProvider(): array + { + return [ + 'as_json' => [ + 'as_json' => true, + 'as_js_varname' => false, + ], + 'as_plain' => [ + 'as_json' => false, + 'as_js_varname' => true, + ], + ]; + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testRemoveProductFromShoppingCart(): void + { + $oldQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $post = $this->hydratePost([ + 'sidebar' => [ + 'remove' => [ + $oldQuote->getItemsCollection()->getFirstItem()->getId() => 'cart', + ], + ], + ]); + $params = $this->hydrateParams(); + + $this->dispatchWitParams($params, $post); + + $this->checkHandles(explode(',', $params['block'])); + $this->checkQuotes($oldQuote); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testClearShoppingCart(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $post = $this->hydratePost([ + 'sidebar' => [ + 'empty_customer_cart' => '1', + ], + ]); + $params = $this->hydrateParams(); + + $this->dispatchWitParams($params, $post); + + $this->checkHandles(explode(',', $params['block'])); + $this->assertEmpty($quote->getItemsCollection(false)->getItems()); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/inactive_quote_with_customer.php + * + * @return void + */ + public function testMoveFromOrderToShoppingCart(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_inactive_quote'); + $this->session->setQuoteId($quote->getId()); + $post = $this->hydratePost([ + 'update_items' => '1', + 'item' => [ + $quote->getItemsCollection()->getFirstItem()->getId() => [ + 'qty' => '1', + 'use_discount' => '1', + 'action' => 'cart', + ], + ], + ]); + $params = $this->hydrateParams(['blocks' => null]); + $this->dispatchWitParams($params, $post); + $customerCart = $this->quoteRepository->getForCustomer(1); + $cartItems = $customerCart->getItemsCollection(); + $this->assertCount(1, $cartItems->getItems()); + $this->assertEquals('taxable_product', $cartItems->getFirstItem()->getSku()); + $this->quoteIdsToRemove[] = $customerCart->getId(); + } + + /** + * Check customer quotes + * + * @param CartInterface $oldQuote + * @param string|null $expectedSku + * @return void + */ + private function checkQuotes(CartInterface $oldQuote, ?string $expectedSku = null): void + { + $newQuote = $this->session->getQuote(); + $oldQuoteItemCollection = $oldQuote->getItemsCollection(false); + $this->assertEmpty($oldQuoteItemCollection->getItems()); + $newQuoteItemsCollection = $newQuote->getItemsCollection(false); + + if ($expectedSku !== null) { + $this->assertNotNull($newQuoteItemsCollection->getItemByColumnValue('sku', $expectedSku)); + } else { + $this->assertEmpty($newQuoteItemsCollection->getItems()); + } + } + + /** + * Check that all required handles were applied + * + * @param array $blocks + * @param bool $asJson + * @return void + */ + private function checkHandles(array $blocks, bool $asJson = true): void + { + $handles = $this->layout->getUpdate()->getHandles(); + + if ($asJson) { + $this->assertContains('sales_order_create_load_block_message', $handles); + $this->assertContains('sales_order_create_load_block_json', $handles); + } else { + $this->assertContains('sales_order_create_load_block_plain', $handles); + } + + foreach ($blocks as $block) { + $this->assertContains( + 'sales_order_create_load_block_' . $block, + $handles + ); + } + } + + /** + * Fill post params array to proper state + * + * @param array $inputArray + * @return array + */ + private function hydratePost(array $inputArray = []): array + { + return array_merge( + [ + 'customer_id' => 1, + 'store_id' => $this->storeManager->getStore('default')->getId(), + 'sidebar' => [], + ], + $inputArray + ); + } + + /** + * Fill params array to proper state + * + * @param array $inputArray + * @return array + */ + private function hydrateParams(array $inputArray = []): array + { + return array_merge( + [ + 'json' => true, + 'block' => 'sidebar,items,shipping_method,billing_method,totals,giftmessage', + 'as_js_varname' => true, + ], + $inputArray + ); + } + + /** + * Dispatch request with params + * + * @param array $params + * @param array $postParams + * @return void + */ + private function dispatchWitParams(array $params, array $postParams): void + { + $this->getRequest()->setMethod(Http::METHOD_POST) + ->setPostValue($postParams) + ->setParams($params); + $this->dispatch('backend/sales/order_create/loadBlock'); + } +} 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 index 726ba697beb12..3c26a53424d81 100644 --- 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 @@ -7,39 +7,34 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; -use Magento\Framework\Api\SearchCriteria; use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Data\Form\FormKey; +use Magento\Framework\App\Request\Http; use Magento\Sales\Api\Data\InvoiceInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\OrderRepository; +use Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\TestCase\AbstractBackendController; /** * Abstract backend invoice test. */ -class AbstractInvoiceControllerTest extends AbstractBackendController +abstract class AbstractInvoiceControllerTest extends AbstractBackendController { - /** - * @var TransportBuilderMock - */ + /** @var TransportBuilderMock */ protected $transportBuilder; - /** - * @var OrderRepository - */ - protected $orderRepository; + /** @var string */ + protected $resource = 'Magento_Sales::sales_invoice'; - /** - * @var FormKey - */ - protected $formKey; + /** @var OrderRepository */ + private $orderRepository; - /** - * @var string - */ - protected $resource = 'Magento_Sales::sales_invoice'; + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var CollectionFactory */ + private $invoiceCollectionFactory; /** * @inheritdoc @@ -47,46 +42,71 @@ class AbstractInvoiceControllerTest extends AbstractBackendController protected function setUp(): void { parent::setUp(); + $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); $this->orderRepository = $this->_objectManager->get(OrderRepository::class); - $this->formKey = $this->_objectManager->get(FormKey::class); + $this->searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $this->invoiceCollectionFactory = $this->_objectManager->get(CollectionFactory::class); } /** + * Retrieve order + * * @param string $incrementalId * @return OrderInterface|null */ - protected function getOrder(string $incrementalId) + protected function getOrder(string $incrementalId): ?OrderInterface { - /** @var SearchCriteria $searchCriteria */ - $searchCriteria = $this->_objectManager->create(SearchCriteriaBuilder::class) - ->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) + $searchCriteria = $this->searchCriteriaBuilder->addFilter(OrderInterface::INCREMENT_ID, $incrementalId) ->create(); - $orders = $this->orderRepository->getList($searchCriteria)->getItems(); - /** @var OrderInterface $order */ - $order = reset($orders); - return $order; + return reset($orders); } /** - * @param OrderInterface $order + * Get firs order invoice + * + * @param OrderInterface|int $order * @return InvoiceInterface */ - protected function getInvoiceByOrder(OrderInterface $order): InvoiceInterface + protected function getInvoiceByOrder($order): InvoiceInterface { - /** @var \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection $invoiceCollection */ - $invoiceCollection = $this->_objectManager->create( - \Magento\Sales\Model\ResourceModel\Order\Invoice\CollectionFactory::class - )->create(); + $invoiceCollection = $this->invoiceCollectionFactory->create(); - /** @var InvoiceInterface $invoice */ - $invoice = $invoiceCollection - ->setOrderFilter($order) - ->setPageSize(1) - ->getFirstItem(); + return $invoiceCollection->setOrderFilter($order)->setPageSize(1)->getFirstItem(); + } - return $invoice; + /** + * Prepare request + * + * @param array $postParams + * @param array $params + * @return void + */ + protected function prepareRequest(array $postParams = [], array $params = []): void + { + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($postParams); + } + + /** + * Normalize post parameters + * + * @param array $items + * @param string $commentText + * @param bool $doShipment + * @return array + */ + protected function hydratePost(array $items, string $commentText = '', $doShipment = false): array + { + return [ + 'invoice' => [ + 'items' => $items, + 'comment_text' => $commentText, + 'do_shipment' => $doShipment + ], + ]; } } 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 index ee59a55acd9b1..c7711e8897696 100644 --- 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 @@ -30,10 +30,11 @@ class AddCommentTest extends AbstractInvoiceControllerTest public function testSendEmailOnAddInvoiceComment(): void { $comment = 'Test Invoice Comment'; - $order = $this->prepareRequest( - [ - 'comment' => ['comment' => $comment, 'is_customer_notified' => true], - ] + $order = $this->getOrder('100000001'); + $invoice = $this->getInvoiceByOrder($order); + $this->prepareRequest( + ['comment' => ['comment' => $comment, 'is_customer_notified' => true]], + ['id' => $invoice->getEntityId()] ); $this->dispatch('backend/sales/order_invoice/addComment'); @@ -41,6 +42,7 @@ public function testSendEmailOnAddInvoiceComment(): void $this->assertStringContainsString($comment, $html); $message = $this->transportBuilder->getSentMessage(); + $this->assertNotNull($message); $subject = __('Update to your %1 invoice', $order->getStore()->getFrontendName())->render(); $messageConstraint = $this->logicalAnd( new StringContains($order->getCustomerName()), @@ -55,7 +57,8 @@ public function testSendEmailOnAddInvoiceComment(): void ); $this->assertEquals($message->getSubject(), $subject); - $this->assertThat($message->getBody()->getParts()[0]->getRawContent(), $messageConstraint); + $bodyParts = $message->getBody()->getParts(); + $this->assertThat(reset($bodyParts)->getRawContent(), $messageConstraint); } /** @@ -63,7 +66,7 @@ public function testSendEmailOnAddInvoiceComment(): void */ public function testAclHasAccess() { - $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + $this->prepareRequest(); parent::testAclHasAccess(); } @@ -73,31 +76,8 @@ public function testAclHasAccess() */ public function testAclNoAccess() { - $this->prepareRequest(['comment' => ['comment' => 'Comment']]); + $this->prepareRequest(); 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/NewActionTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewActionTest.php new file mode 100644 index 0000000000000..c8444c827d2e4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/NewActionTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use Magento\Framework\App\Request\Http; +use Magento\Framework\Escaper; +use Magento\Framework\Message\MessageInterface; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for new invoice action + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Invoice\NewAction + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class NewActionTest extends AbstractBackendController +{ + /** @var OrderFactory */ + private $orderFactory; + + /** @var Escaper */ + private $escaper; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + $this->escaper = $this->_objectManager->get(Escaper::class); + } + + /** + * @return void + */ + public function testWithNoExistingOrder(): void + { + $this->dispatchWithOrderId(863521); + $expectedMessage = (string)__("The entity that was requested doesn't exist. Verify the entity and try again."); + $this->assertSessionMessages($this->containsEqual($this->escaper->escapeHtml($expectedMessage))); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php + * + * @return void + */ + public function testCanNotInvoice(): void + { + $expectedMessage = __('The order does not allow an invoice to be created.'); + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->dispatchWithOrderId((int)$order->getEntityId()); + $this->assertSessionMessages($this->containsEqual((string)$expectedMessage), MessageInterface::TYPE_ERROR); + } + + /** + * Dispatch request with order_id param + * + * @param int $orderId + * @return void + */ + private function dispatchWithOrderId(int $orderId): void + { + $this->getRequest()->setMethod(Http::METHOD_GET) + ->setParams(['order_id' => $orderId]); + $this->dispatch('backend/sales/order_invoice/new'); + } +} 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 index 2dc5f5adc86d2..13003e40dc0a3 100644 --- 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 @@ -7,38 +7,53 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; +use Magento\Framework\Escaper; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Model\Order; use PHPUnit\Framework\Constraint\StringContains; /** - * Class tests invoice creation in backend. + * Class tests invoice creation in admin panel. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Invoice\Save * * @magentoDbIsolation enabled * @magentoAppArea adminhtml - * @magentoDataFixture Magento/Sales/_files/order.php */ class SaveTest extends AbstractInvoiceControllerTest { + /** @var string */ + protected $uri = 'backend/sales/order_invoice/save'; + + /** @var Escaper */ + private $escaper; + /** - * @var string + * @inheritdoc */ - protected $uri = 'backend/sales/order_invoice/save'; + protected function setUp(): void + { + parent::setUp(); + + $this->escaper = $this->_objectManager->get(Escaper::class); + } /** + * @magentoDataFixture Magento/Sales/_files/order.php + * * @return void */ public function testSendEmailOnInvoiceSave(): void { - $order = $this->prepareRequest(); + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $post = $this->hydratePost([$itemId => 2]); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); $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); + $this->checkSuccess($invoice, 2); $message = $this->transportBuilder->getSentMessage(); + $this->assertNotNull($message); $subject = __('Invoice for your %1 order', $order->getStore()->getFrontendName())->render(); $messageConstraint = $this->logicalAnd( new StringContains($invoice->getBillingAddress()->getName()), @@ -49,9 +64,113 @@ public function testSendEmailOnInvoiceSave(): void "Your Invoice #{$invoice->getIncrementId()} for Order #{$order->getIncrementId()}" ) ); - $this->assertEquals($message->getSubject(), $subject); - $this->assertThat($message->getBody()->getParts()[0]->getRawContent(), $messageConstraint); + $bodyParts = $message->getBody()->getParts(); + $this->assertThat(reset($bodyParts)->getRawContent(), $messageConstraint); + } + + /** + * @magentoConfigFixture current_store sales_email/invoice/enabled 0 + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testSendEmailOnInvoiceSaveWithDisabledConfig(): void + { + $order = $this->getOrder('100000001'); + $post = $this->hydratePost([$order->getItemsCollection()->getFirstItem()->getId() => 2]); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->checkSuccess($this->getInvoiceByOrder($order), 2); + $this->assertNull($this->transportBuilder->getSentMessage()); + } + + /** + * @dataProvider invoiceDataProvider + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @param int $invoicedItemsQty + * @param string $commentMessage + * @param bool $doShipment + * @return void + */ + public function testSuccessfulInvoice( + int $invoicedItemsQty, + string $commentMessage = '', + bool $doShipment = false + ): void { + $order = $this->getOrder('100000001'); + $post = $this->hydratePost( + [$order->getItemsCollection()->getFirstItem()->getId() => $invoicedItemsQty], + $commentMessage, + $doShipment + ); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->checkSuccess($this->getInvoiceByOrder($order), $invoicedItemsQty, $commentMessage, $doShipment); + } + + /** + * @return array + */ + public function invoiceDataProvider(): array + { + return [ + 'with_comment_message' => [ + 'invoiced_items_qty' => 2, + 'comment_message' => 'test comment message', + ], + 'partial_invoice' => [ + 'invoiced_items_qty' => 1, + ], + 'with_do_shipment' => [ + 'invoiced_items_qty' => 2, + 'comment_message' => '', + 'do_shipment' => true, + ], + ]; + } + + /** + * @return void + */ + public function testWitNoExistingOrder(): void + { + $expectedMessage = (string)__('The order no longer exists.'); + $this->prepareRequest(['order_id' => 899989]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->assertErrorResponse($expectedMessage); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php + * + * @return void + */ + public function testCanNotInvoiceOrder(): void + { + $expectedMessage = (string)__('The order does not allow an invoice to be created.'); + $order = $this->getOrder('100000001'); + $this->prepareRequest([], ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->assertErrorResponse($expectedMessage); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testInvoiceWithoutQty(): void + { + $expectedMessage = (string)__('The invoice can\'t be created without products. Add products and try again.'); + $order = $this->getOrder('100000001'); + $post = $this->hydratePost([$order->getItemsCollection()->getFirstItem()->getId() => '0']); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/save'); + $this->assertErrorResponse($this->escaper->escapeHtml($expectedMessage)); } /** @@ -75,23 +194,64 @@ public function testAclNoAccess() } /** - * @param array $params - * @return \Magento\Sales\Api\Data\OrderInterface|null + * Checks that order protect code is not changing after invoice submitting + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void */ - private function prepareRequest(array $params = []) + public function testOrderProtectCodePreserveAfterInvoiceSave(): void { $order = $this->getOrder('100000001'); - $this->getRequest()->setMethod('POST'); - $this->getRequest()->setParams( - [ - 'order_id' => $order->getEntityId(), - 'form_key' => $this->formKey->getFormKey(), - ] - ); + $this->prepareRequest([], ['order_id' => $order->getEntityId()]); + $protectCode = $order->getProtectCode(); + $this->dispatch($this->uri); + $invoicedOrder = $this->getOrder('100000001'); + + $this->assertEquals($protectCode, $invoicedOrder->getProtectCode()); + } + + /** + * Check error response + * + * @param string $expectedMessage + * @return void + */ + private function assertErrorResponse(string $expectedMessage): void + { + $this->assertRedirect($this->stringContains('sales/order_invoice/new')); + $this->assertSessionMessages($this->containsEqual($expectedMessage)); + } - $data = $params ?? []; - $this->getRequest()->setPostValue($data); + /** + * Check that invoice was successfully created + * + * @param InvoiceInterface $invoice + * @param int $invoicedItemsQty + * @param string|null $commentMessage + * @param bool $doShipment + * @return void + */ + private function checkSuccess( + InvoiceInterface $invoice, + int $invoicedItemsQty, + ?string $commentMessage = null, + bool $doShipment = false + ): void { + $message = $doShipment ? 'You created the invoice and shipment.' : 'The invoice has been created.'; + $expectedState = $doShipment ? Order::STATE_COMPLETE : Order::STATE_PROCESSING; + $this->assertNotNull($invoice->getEntityId()); + $this->assertEquals($invoicedItemsQty, (int)$invoice->getTotalQty()); + $order = $invoice->getOrder(); + $this->assertEquals($expectedState, $order->getState()); - return $order; + if ($commentMessage) { + $this->assertEquals($commentMessage, $invoice->getCustomerNote()); + } + + $this->assertRedirect( + $this->stringContains(sprintf('sales/order/view/order_id/%u', (int)$order->getEntityId())) + ); + $this->assertSessionMessages($this->containsEqual((string)__($message))); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php new file mode 100644 index 0000000000000..5eb554ef937d5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/StartTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use Magento\Backend\Model\Session; +use Magento\Framework\App\Request\Http; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for invoice start action + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Invoice\Start + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class StartTest extends AbstractBackendController +{ + /** @var OrderFactory */ + private $orderFactory; + + /** @var Session */ + private $session; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + $this->session = $this->_objectManager->get(Session::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->session->getInvoiceItemQtys(true); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testExecute(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->session->setInvoiceItemQtys('test'); + $this->getRequest()->setMethod(Http::METHOD_GET)->setParams(['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/start'); + $this->assertRedirect($this->stringContains('sales/order_invoice/new')); + $this->assertNull($this->session->getInvoiceItemQtys()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php new file mode 100644 index 0000000000000..2b91c5d04fd6f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/UpdateQtyTest.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\TestFramework\Helper\Xpath; + +/** + * Class tests invoice items qty update. + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ +class UpdateQtyTest extends AbstractInvoiceControllerTest +{ + /** @var SerializerInterface */ + private $json; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->json = $this->_objectManager->get(SerializerInterface::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testSuccess(): void + { + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $qtyToInvoice = 1; + $invoicedItemsXpath = sprintf( + "//input[contains(@class, 'qty-input') and @name='invoice[items][%u]' and @value='%u']", + $itemId, + $qtyToInvoice + ); + $post = $this->hydratePost([$itemId => $qtyToInvoice]); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/updateQty'); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($invoicedItemsXpath, $this->getResponse()->getContent()) + ); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order_with_bundle_and_invoiced.php + * + * @return void + */ + public function testCanNotInvoice(): void + { + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $post = $this->hydratePost([$itemId => '1']); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/updateQty'); + $this->assertErrorResponse('The order does not allow an invoice to be created.'); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testWithoutQty(): void + { + $order = $this->getOrder('100000001'); + $itemId = $order->getItemsCollection()->getFirstItem()->getId(); + $post = $this->hydratePost([$itemId => '0']); + $this->prepareRequest($post, ['order_id' => $order->getEntityId()]); + $this->dispatch('backend/sales/order_invoice/updateQty'); + $this->assertErrorResponse( + 'The invoice can\'t be created without products. Add products and try again.' + ); + } + + /** + * @return void + */ + public function testWithNoExistingOrderId(): void + { + $post = $this->hydratePost([ + 'invoice' => [ + 'items' => [ + '1' => '3', + ], + ], + ]); + $this->prepareRequest($post, ['order_id' => 6543265]); + $this->dispatch('backend/sales/order_invoice/updateQty'); + $this->assertErrorResponse('The order no longer exists.'); + } + + /** + * Check error response + * + * @param string $expectedMessage + * @return void + */ + private function assertErrorResponse(string $expectedMessage): void + { + $expectedResponse = [ + 'error' => true, + 'message' => (string)__($expectedMessage), + ]; + $response = $this->getResponse()->getContent(); + $this->assertNotEmpty($response); + $this->assertEquals($expectedResponse, $this->json->unserialize($response)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php new file mode 100644 index 0000000000000..cffdda80cc897 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Guest/ReorderTest.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Guest; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Helper\Guest; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test for guest reorder controller. + * + * @see \Magento\Sales\Controller\Guest\Reorder + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ReorderTest extends AbstractController +{ + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var CookieManagerInterface */ + private $cookieManager; + + /** @var Session */ + private $customerSession; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->checkoutSession = $this->_objectManager->get(CheckoutSession::class); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->cookieManager = $this->_objectManager->get(CookieManagerInterface::class); + $this->customerSession = $this->_objectManager->get(Session::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $createdQuoteId = $this->checkoutSession->getQuoteId(); + + if ($createdQuoteId !== null) { + try { + $this->quoteRepository->delete($this->quoteRepository->get($createdQuoteId)); + } catch (NoSuchEntityException $e) { + //already deleted + } + } + + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDbIsolation disabled + * + * @magentoDataFixture Magento/Sales/_files/order_by_guest_with_simple_product.php + * + * @return void + */ + public function testReorderSimpleProduct(): void + { + $orderIncrementId = 'test_order_1'; + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + $cookieValue = base64_encode($order->getProtectCode() . ':' . $orderIncrementId); + $this->cookieManager->setPublicCookie(Guest::COOKIE_NAME, $cookieValue); + $this->dispatchReorderRequest(); + $this->assertRedirect($this->stringContains('checkout/cart')); + $quoteId = $this->checkoutSession->getQuoteId(); + $this->assertNotNull($quoteId); + $quoteItemsCollection = $this->quoteRepository->get((int)$quoteId)->getItemsCollection(); + $this->assertCount(1, $quoteItemsCollection); + $this->assertEquals( + $order->getItemsCollection()->getFirstItem()->getSku(), + $quoteItemsCollection->getFirstItem()->getSku() + ); + } + + /** + * @return void + */ + public function testReorderWithoutParamsAndCookie(): void + { + $this->dispatchReorderRequest(); + $this->assertRedirect($this->stringContains('sales/guest/form')); + $this->assertSessionMessages( + $this->containsEqual((string)__('You entered incorrect data. Please try again.')), + MessageInterface::TYPE_ERROR + ); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testReorderGuestOrderByCustomer(): void + { + $this->customerSession->setCustomerId(1); + $this->dispatchReorderRequest(); + $this->assertRedirect($this->stringContains('sales/order/history')); + } + + /** + * Dispatch reorder request. + * + * @return void + */ + private function dispatchReorderRequest(): void + { + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->dispatch('sales/guest/reorder/'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php new file mode 100644 index 0000000000000..3b32e7238cc76 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Order; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session; +use Magento\Framework\Escaper; +use Magento\Framework\Message\MessageInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Core\Version\View; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test for reorder controller. + * + * @see \Magento\Sales\Controller\Order\Reorder + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ReorderTest extends AbstractController +{ + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var Session */ + private $customerSession; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CartInterface */ + private $quote; + + /** @var Escaper */ + private $escaper; + + /** + * @var View + */ + private $versionChecker; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->checkoutSession = $this->_objectManager->get(CheckoutSession::class); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->customerSession = $this->_objectManager->get(Session::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $this->escaper = $this->_objectManager->get(Escaper::class); + $this->versionChecker = $this->_objectManager->get(View::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/customer_order_with_taxable_product.php + * + * @return void + */ + public function testReorder(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('test_order_with_taxable_product'); + $this->customerSession->setCustomerId($order->getCustomerId()); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('checkout/cart')); + $this->quote = $this->checkoutSession->getQuote(); + $quoteItemsCollection = $this->quote->getItemsCollection(); + $this->assertCount(1, $quoteItemsCollection); + $this->assertEquals( + $order->getItemsCollection()->getFirstItem()->getSku(), + $quoteItemsCollection->getFirstItem()->getSku() + ); + } + + /** + * @magentoDataFixture Magento/Sales/_files/customer_order_with_simple_product.php + * + * @return void + */ + public function testReorderProductLowQty(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('55555555'); + $this->customerSession->setCustomerId($order->getCustomerId()); + $this->dispatchReorderRequest((int)$order->getId()); + $origMessage = (string)__('The requested qty is not available'); + $message = $this->escaper->escapeHtml( + __('Could not add the product with SKU "%1" to the shopping cart: %2', 'simple-1', $origMessage) + ); + $constraint = $this->logicalOr($this->containsEqual($origMessage), $this->containsEqual($message)); + $this->assertThat($this->getMessages(MessageInterface::TYPE_ERROR), $constraint); + $this->quote = $this->checkoutSession->getQuote(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Sales/_files/customer_order_with_two_items.php + * + * @return void + */ + public function testReorderByAnotherCustomer(): void + { + $this->customerSession->setCustomerId(1); + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->dispatchReorderRequest((int)$order->getId()); + + if ($this->versionChecker->isVersionUpdated()) { + $this->assertRedirect($this->stringContains('noroute')); + } else { + $this->assertRedirect($this->stringContains('sales/order/history')); + } + } + + /** + * Dispatch reorder request. + * + * @param null|int $orderId + * @return void + */ + private function dispatchReorderRequest(?int $orderId = null): void + { + $this->getRequest()->setMethod(Request::METHOD_POST); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('sales/order/reorder/'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php new file mode 100644 index 0000000000000..5a21f551ff1a7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Helper; + +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for reorder helper. + * + * @see \Magento\Sales\Helper\Reorder + * @magentoDbIsolation enabled + */ +class ReorderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Reorder */ + private $helper; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var Session */ + private $customerSession; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(Reorder::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->customerSession = $this->objectManager->get(Session::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->customerSession->setCustomerId(null); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testCanReorderForGuest(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->assertTrue($this->helper->canReorder($order->getId())); + } + + /** + * @magentoDataFixture Magento/Sales/_files/customer_order_with_two_items.php + * + * @return void + */ + public function testCanReorderForLoggedCustomer(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $this->customerSession->setCustomerId($order->getCustomerId()); + $this->assertTrue($this->helper->canReorder($order->getId())); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Sales/_files/order_state_hold.php + * + * @return void + */ + public function testCanReorderHoldOrderForLoggedCustomer(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->customerSession->setCustomerId(1); + $this->assertFalse($this->helper->canReorder($order->getId())); + } + + /** + * @magentoConfigFixture current_store sales/reorder/allow 0 + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testCanReorderConfigDisabled(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->assertFalse($this->helper->canReorder($order->getId())); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php index e1cc942d4ae28..3e6b27a7ca622 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php @@ -684,7 +684,6 @@ public function testMoveQuoteItemToCart() public function testGetCustomerCartNewCart() { $customerIdFromFixture = 1; - $customerEmailFromFixture = 'customer@example.com'; /** Preconditions */ /** @var SessionQuote $session */ @@ -693,12 +692,8 @@ public function testGetCustomerCartNewCart() /** SUT execution */ $customerQuote = $this->model->getCustomerCart(); - self::assertNotEmpty($customerQuote->getId(), 'Quote ID is invalid.'); - self::assertEquals( - $customerEmailFromFixture, - $customerQuote->getCustomerEmail(), - 'Customer data is preserved incorrectly in a newly quote.' - ); + self::assertInstanceOf(Quote::class, $customerQuote); + self::assertEmpty($customerQuote->getData()); } /** diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.php new file mode 100644 index 0000000000000..ca102b0fabf89 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product.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\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/customer_quote_ready_for_order.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CartManagementInterface $quoteManagement */ +$quoteManagement = $objectManager->get(CartManagementInterface::class); + +$quote = $quoteRepository->getActiveForCustomer(1); +$quoteManagement->placeOrder($quote->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_rollback.php new file mode 100644 index 0000000000000..46cabc2e3fd9b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_simple_product_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\Registry; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderInterfaceFactory::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$order = $orderFactory->create()->loadByIncrementId('55555555'); +if ($order->getId()) { + $orderRepository->delete($order); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/customer_quote_ready_for_order_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php new file mode 100644 index 0000000000000..59ec4182ac870 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product.php @@ -0,0 +1,30 @@ +<?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\Api\Data\PaymentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_taxable_product_and_customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CartManagementInterface $quoteManagement */ +$quoteManagement = $objectManager->get(CartManagementInterface::class); +/** @var PaymentInterface $payment */ +$payment = $objectManager->get(PaymentInterface::class); +$payment->setMethod('checkmo'); + +$quote = $quoteRepository->getActiveForCustomer(1); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getShippingAddress()->collectShippingRates(); +$quoteRepository->save($quote); +$quoteManagement->placeOrder($quote->getId(), $payment); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_product_rollback.php new file mode 100644 index 0000000000000..d42f6a1140286 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_taxable_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\Framework\Registry; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderInterfaceFactory::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$order = $orderFactory->create()->loadByIncrementId('test_order_with_taxable_product'); +if ($order->getId()) { + $orderRepository->delete($order); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture( + 'Magento/Checkout/_files/quote_with_taxable_product_and_customer_rollback.php' +); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php new file mode 100644 index 0000000000000..c3bab9acca27b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product.php @@ -0,0 +1,31 @@ +<?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\Api\Data\PaymentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address_saved.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CartManagementInterface $quoteManagement */ +$quoteManagement = $objectManager->get(CartManagementInterface::class); +/** @var PaymentInterface $payment */ +$payment = $objectManager->get(PaymentInterface::class); + +$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('test_order_1'); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getShippingAddress()->collectShippingRates(); +$quoteRepository->save($quote); +$payment->setMethod('checkmo'); +$quoteManagement->placeOrder($quote->getId(), $payment); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_rollback.php new file mode 100644 index 0000000000000..b4ec514d1311e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_by_guest_with_simple_product_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\Registry; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderInterfaceFactory::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$order = $orderFactory->create()->loadByIncrementId('test_order_1'); +if ($order->getId()) { + $orderRepository->delete($order); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address_saved_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php new file mode 100644 index 0000000000000..954c23498ec66 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsCsvTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCoupons; + +use Magento\Framework\App\ResourceConnection; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection as RuleCollection; +use Magento\SalesRule\Model\Rule; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test export coupon csv + * + * Verify export csv + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_with_coupon_list.php + */ +class ExportCouponsCsvTest extends AbstractBackendController +{ + /** + * @var string + */ + protected $uri = 'backend/sales_rule/promo_quote/exportCouponsCsv'; + + /** + * @var string + */ + protected $resource = 'Magento_SalesRule::quote'; + + /** + * @var Rule + */ + private $salesRule; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->resourceConnection = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $this->initSalesRule(); + } + + /** + * Prepare request + * + * @return void + */ + private function prepareRequest(): void + { + $couponList = $this->getCouponsIdList(); + if (count($couponList)) { + $this->getRequest()->setParams(['internal_ids' => $couponList[0]])->setMethod('POST'); + } + } + + /** + * Init current sales rule + * + * @return void + */ + private function initSalesRule(): void + { + /** @var RuleCollection $collection */ + $collection = Bootstrap::getObjectManager()->create(RuleCollection::class); + $collection->addFieldToFilter('name', 'Rule with coupon list'); + $this->salesRule = $collection->getFirstItem(); + } + + /** + * Retrieve id list of coupons + * + * @return array + */ + private function getCouponsIdList(): array + { + $select = $this->resourceConnection->getConnection() + ->select() + ->from($this->resourceConnection->getTableName('salesrule_coupon')) + ->columns(['coupon_id']) + ->where('rule_id=?', $this->salesRule->getId()); + + return $this->resourceConnection->getConnection()->fetchCol($select); + } + + /** + * Test export csv + * + * @return void + */ + public function testExportCsv(): void + { + $this->prepareRequest(); + $this->dispatch($this->uri); + $this->assertStringNotContainsString('404 Error', $this->getResponse()->getBody()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php new file mode 100644 index 0000000000000..d222b064a0d2c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Controller/Adminhtml/Promo/Quote/ExportCoupons/ExportCouponsXmlTest.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Controller\Adminhtml\Promo\Quote\ExportCoupons; + +use Magento\Framework\App\ResourceConnection; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection as RuleCollection; +use Magento\SalesRule\Model\Rule; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test export coupon xml + * + * Verify export xml + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_with_coupon_list.php + */ +class ExportCouponsXmlTest extends AbstractBackendController +{ + /** + * @var string + */ + protected $uri = 'backend/sales_rule/promo_quote/exportCouponsXml'; + + /** + * @var string + */ + protected $resource = 'Magento_SalesRule::quote'; + + /** + * @var Rule + */ + private $salesRule; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->resourceConnection = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $this->initSalesRule(); + } + + /** + * Prepare request + * + * @return void + */ + private function prepareRequest(): void + { + $couponList = $this->getCouponsIdList(); + if (count($couponList)) { + $this->getRequest()->setParams(['internal_ids' => $couponList[0]])->setMethod('POST'); + } + } + + /** + * Init current sales rule + * + * @return void + */ + private function initSalesRule(): void + { + /** @var RuleCollection $collection */ + $collection = Bootstrap::getObjectManager()->create(RuleCollection::class); + $collection->addFieldToFilter('name', 'Rule with coupon list'); + $this->salesRule = $collection->getFirstItem(); + } + + /** + * Retrieve id list of coupons + * + * @return array + */ + private function getCouponsIdList(): array + { + $select = $this->resourceConnection->getConnection() + ->select() + ->from($this->resourceConnection->getTableName('salesrule_coupon')) + ->columns(['coupon_id']) + ->where('rule_id=?', $this->salesRule->getId()); + + return $this->resourceConnection->getConnection()->fetchCol($select); + } + + /** + * Test export xml + * + * @return void + */ + public function testExportCsv(): void + { + $this->prepareRequest(); + $this->dispatch($this->uri); + $this->assertStringNotContainsString('404 Error', $this->getResponse()->getBody()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php index 3ec185e71a1e5..355239f9dc2e7 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php @@ -87,6 +87,9 @@ protected function tearDown(): void { $this->setIncrement(1); + self::restoreFromDb(); + self::$dbRestored = true; + parent::tearDown(); } diff --git a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php index 2829cffd8d8a7..10e5cf40bd500 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php @@ -75,6 +75,8 @@ protected function tearDown(): void $indexer = $this->indexerRegistry->get($indexerId); $indexer->setScheduled($state); } + self::restoreFromDb(); + self::$dbRestored = true; } public static function setUpBeforeClass(): void diff --git a/dev/tests/integration/testsuite/Magento/Store/Ui/Component/Listing/Column/Store/OptionsTest.php b/dev/tests/integration/testsuite/Magento/Store/Ui/Component/Listing/Column/Store/OptionsTest.php new file mode 100644 index 0000000000000..e13c4a427464f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/Ui/Component/Listing/Column/Store/OptionsTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Ui\Component\Listing\Column\Store; + +use Magento\Store\Model\ResourceModel\Group as GroupResource; +use Magento\Store\Model\ResourceModel\Store as StoreResource; +use Magento\Store\Model\ResourceModel\Website as WebsiteResource; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Store\Ui\Component\Listing\Column\Store\Options. + */ +class OptionsTest extends TestCase +{ + private const DEFAULT_WEBSITE_NAME = 'Main Website'; + private const DEFAULT_STORE_GROUP_NAME = 'Main Website Store'; + private const DEFAULT_STORE_NAME = 'Default Store View'; + + /** + * @var OptionsFactory + */ + private $modelFactory; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var WebsiteResource + */ + private $websiteResource; + + /** + * @var StoreResource + */ + private $storeResource; + + /** + * @var GroupResource + */ + private $groupResource; + + /** + * @return void + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + + $this->modelFactory = $objectManager->get(OptionsFactory::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); + + $this->websiteResource = $objectManager->get(WebsiteResource::class); + $this->groupResource = $objectManager->get(GroupResource::class); + $this->storeResource = $objectManager->get(StoreResource::class); + } + + /** + * To option array test with duplicate website, store group, store view names + * + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * + * @return void + */ + public function testToOptionArray(): void + { + $website = $this->storeManager->getWebsite('test'); + $this->websiteResource->save($website->setName(self::DEFAULT_WEBSITE_NAME)); + + $storeGroup = current($website->getGroups()); + $this->groupResource->save($storeGroup->setName(self::DEFAULT_STORE_GROUP_NAME)); + + $store = current($website->getStores()); + $this->storeResource->save($store->setName(self::DEFAULT_STORE_NAME)); + + $model = $this->modelFactory->create(); + $storeIds = [$this->storeManager->getStore('default')->getId(), $store->getId()]; + + $this->assertEquals($this->getExpectedOptions($storeIds), $model->toOptionArray()); + } + + /** + * Returns expected options + * + * @param array $storeIds + * @return array + */ + private function getExpectedOptions(array $storeIds): array + { + $expectedOptions = []; + foreach ($storeIds as $storeId) { + $expectedOptions[] = [ + 'label' => self::DEFAULT_WEBSITE_NAME, + 'value' => [[ + 'label' => str_repeat(' ', 4) . self::DEFAULT_STORE_GROUP_NAME, + 'value' => [[ + 'label' => str_repeat(' ', 8) . self::DEFAULT_STORE_NAME, + 'value' => $storeId, + ]], + ]], + ]; + } + + return $expectedOptions; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/core_second_third_fixturestore_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/core_second_third_fixturestore_rollback.php index 19d064bb79834..0ccfdf0af5c94 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/core_second_third_fixturestore_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/core_second_third_fixturestore_rollback.php @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; +use Magento\UrlRewrite\Model\UrlRewrite; + /** @var \Magento\Framework\Registry $registry */ $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); @@ -33,5 +38,21 @@ $store->delete(); } +$urlRewriteCollectionFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + UrlRewriteCollectionFactory::class +); +/** @var UrlRewriteCollection $urlRewriteCollection */ +$urlRewriteCollection = $urlRewriteCollectionFactory->create(); +$urlRewriteCollection->addFieldToFilter('store_id', ['gt' => 1]); +$urlRewrites = $urlRewriteCollection->getItems(); +/** @var UrlRewrite $urlRewrite */ +foreach ($urlRewrites as $urlRewrite) { + try { + $urlRewrite->delete(); + } catch (\Exception $exception) { + // already removed + } +} + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_rollback.php index 56ba31fad4ed2..a434321189015 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_rollback.php @@ -4,6 +4,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; +use Magento\UrlRewrite\Model\UrlRewrite; + /** @var \Magento\Framework\Registry $registry */ $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); @@ -15,6 +20,24 @@ $store->load('fixture_second_store'); if ($store->getId()) { + $storeId = $store->getId(); + + $urlRewriteCollectionFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + UrlRewriteCollectionFactory::class + ); + /** @var UrlRewriteCollection $urlRewriteCollection */ + $urlRewriteCollection = $urlRewriteCollectionFactory->create(); + $urlRewriteCollection->addFieldToFilter('store_id', ['eq' => $storeId]); + $urlRewrites = $urlRewriteCollection->getItems(); + /** @var UrlRewrite $urlRewrite */ + foreach ($urlRewrites as $urlRewrite) { + try { + $urlRewrite->delete(); + } catch (\Exception $exception) { + // already removed + } + } + $store->delete(); } diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php index 3151a76327397..547ce78500f49 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_currency_rollback.php @@ -4,24 +4,35 @@ * See COPYING.txt for license details. */ declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Directory\Model\Currency as ModelCurrency; +use Magento\Directory\Model\ResourceModel\Currency as ResourceCurrency; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$store = $objectManager->create(\Magento\Store\Model\Store::class); +$objectManager = Bootstrap::getObjectManager(); +$store = $objectManager->create(Store::class); $storeId = $store->load('fixture_second_store', 'code')->getId(); if ($storeId) { - $configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); + $configResource = $objectManager->get(Config::class); $configResource->deleteConfig( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_DEFAULT, - \Magento\Store\Model\ScopeInterface::SCOPE_STORES, + ModelCurrency::XML_PATH_CURRENCY_DEFAULT, + ScopeInterface::SCOPE_STORES, $storeId ); $configResource->deleteConfig( - \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_ALLOW, - \Magento\Store\Model\ScopeInterface::SCOPE_STORES, + ModelCurrency::XML_PATH_CURRENCY_ALLOW, + ScopeInterface::SCOPE_STORES, $storeId ); } Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store_rollback.php'); +$reflectionClass = new \ReflectionClass(ResourceCurrency::class); +$staticProperty = $reflectionClass->getProperty('_rateCache'); +$staticProperty->setAccessible(true); +$staticProperty->setValue(null); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/store_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/store_rollback.php index 8289244d6581a..fa7d18124fdf1 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/store_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/store_rollback.php @@ -7,6 +7,9 @@ use Magento\Framework\Registry; use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewrite; $objectManager = Bootstrap::getObjectManager(); @@ -29,5 +32,23 @@ $store->delete(); } +/** @var UrlRewriteCollectionFactory $urlRewriteCollectionFactory */ +$urlRewriteCollectionFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + UrlRewriteCollectionFactory::class +); +/** @var UrlRewriteCollection $urlRewriteCollection */ +$urlRewriteCollection = $urlRewriteCollectionFactory->create(); +$urlRewriteCollection + ->addFieldToFilter('store_id', ['nin' => [0, 1]]); +$urlRewrites = $urlRewriteCollection->getItems(); +/** @var UrlRewrite $urlRewrite */ +foreach ($urlRewrites as $urlRewrite) { + try { + $urlRewrite->delete(); + } catch (\Exception $exception) { + // already removed + } +} + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php index 5d1758f578836..dd715ecc93b0d 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Block/Product/Renderer/Configurable/PriceTest.php @@ -116,6 +116,7 @@ public function childProductsDataProvider(): array ], 'expected_data' => [ [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 50], 'finalPrice' => ['amount' => 50], @@ -123,6 +124,7 @@ public function childProductsDataProvider(): array 'msrpPrice' => ['amount' => null], ], [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 58.55], 'finalPrice' => ['amount' => 58.55], @@ -130,6 +132,7 @@ public function childProductsDataProvider(): array 'msrpPrice' => ['amount' => null], ], [ + 'baseOldPrice' => ['amount' => 150], 'oldPrice' => ['amount' => 150], 'basePrice' => ['amount' => 75], 'finalPrice' => ['amount' => 75], diff --git a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php index 96fc92c3c0ca5..a119b6259b5f6 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Model/Sales/Total/Quote/TaxTest.php @@ -5,8 +5,9 @@ */ namespace Magento\Tax\Model\Sales\Total\Quote; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Model\Quote\TotalsCollector; -use Magento\Tax\Model\Calculation; use Magento\TestFramework\Helper\Bootstrap; require_once __DIR__ . '/SetupUtil.php'; @@ -15,6 +16,9 @@ /** * Class TaxTest + * + * Tests sales taxes with discounts/price rules during checkout. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TaxTest extends \Magento\TestFramework\Indexer\TestCase @@ -302,6 +306,11 @@ public function testTaxCalculation($configData, $quoteData, $expectedResults) $quoteAddress = $quote->getShippingAddress(); $this->totalsCollector->collectAddressTotals($quote, $quoteAddress); $this->verifyResult($quoteAddress, $expectedResults); + + $skus = array_map(function ($item) { + return $item['sku']; + }, $quoteData['items'] ?? []); + $this->removeProducts($skus); } /** @@ -315,4 +324,32 @@ public function taxDataProvider() global $taxCalculationData; return $taxCalculationData; } + + /** + * Cleanup test by removing products. + * + * @param string[] $skus + * @return void + */ + private function removeProducts(array $skus): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->create(ProductRepositoryInterface::class); + $registry = $objectManager->get(\Magento\Framework\Registry::class); + /** @var ProductRepositoryInterface $productRepository */ + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + foreach ($skus as $sku) { + try { + $productRepository->deleteById($sku); + } catch (NoSuchEntityException $e) { + // product already deleted + } + } + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } } diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrite_rollback.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrite_rollback.php index 4d2c148141943..d8bf0e6bfb731 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrite_rollback.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/_files/url_rewrite_rollback.php @@ -5,9 +5,13 @@ */ declare(strict_types=1); +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + /** @var \Magento\Framework\Registry $registry */ $registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store_rollback.php'); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php index 9a95ed4fd462d..1cbdf6144d640 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php @@ -62,6 +62,23 @@ public function testLoadedProductAttributes() $this->assertEquals('Short description', $productOnWishlist->getData('short_description')); } + /** + * Tests collection load. + * Tests collection load method when product salable filter flag is setted to true + * and few products are present. + * + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * @magentoDbIsolation disabled + */ + public function testLoadWhenFewProductsPresent() + { + $this->itemCollection->setSalableFilter(true); + $this->itemCollection->addCustomerIdFilter(1); + $this->itemCollection->load(); + $this->assertCount(1, $this->itemCollection->getItems()); + } + /** * @param array $attributes */ diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_rollback.php index ee47961dec55b..ac1d846563188 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_rollback.php @@ -7,6 +7,9 @@ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); + /** @var \Magento\Framework\ObjectManagerInterface $objectManager */ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product_rollback.php index 665644cd9b6db..9e46bc60b9a79 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product_rollback.php @@ -13,6 +13,10 @@ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; $objectManager = Bootstrap::getObjectManager(); + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/simple_product_disabled_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); + /** @var Registry $registry */ $registry = $objectManager->get(Registry::class); /** @var WishlistResource $wishListResource */ @@ -28,6 +32,3 @@ $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); - -Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/simple_product_disabled.php'); -Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_simple_product_rollback.php new file mode 100644 index 0000000000000..8ce2f4b64d851 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_simple_product_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Wishlist/_files/wishlist_rollback.php'); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.test.js index 546392d35fe84..a555f8e00a916 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/ConfigurableProduct/view/adminhtml/web/js/components/dynamic-rows-configurable.test.js @@ -41,11 +41,9 @@ define([ }) }; - model.getChildItems = jasmine.createSpy().and.returnValue($('')); model.source = sourceMock; model.processingUnionInsertData(mockData); expect(model.source.get).toHaveBeenCalled(); - expect(model.getChildItems).toHaveBeenCalled(); expect(expectedData[1].sku).toBe('Conf&-sdfs'); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/customer-data.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/customer-data.test.js new file mode 100644 index 0000000000000..7063b846ed166 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/customer-data.test.js @@ -0,0 +1,213 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/*eslint-disable max-nested-callbacks*/ +/*jscs:disable jsDoc*/ + +define([ + 'jquery', + 'underscore', + 'Magento_Customer/js/section-config', + 'Magento_Customer/js/customer-data' +], function ( + $, + _, + sectionConfig, + customerData +) { + 'use strict'; + + var sectionConfigSettings = { + baseUrls: [ + 'http://localhost/' + ], + sections: { + 'customer/account/loginpost': ['*'], + 'checkout/cart/add': ['cart'], + 'rest/*/v1/guest-carts/*/selected-payment-method': ['cart','checkout-data'], + '*': ['messages'] + }, + clientSideSections: [ + 'checkout-data', + 'cart-data' + ], + sectionNames: [ + 'customer', + 'product_data_storage', + 'cart', + 'messages' + ] + }, + cookieLifeTime = 3600, + jQueryGetJSON; + + function init(config) { + var defaultConfig = { + sectionLoadUrl: 'http://localhost/customer/section/load/', + expirableSectionLifetime: 60, // minutes + expirableSectionNames: ['cart'], + cookieLifeTime: cookieLifeTime, + updateSessionUrl: 'http://localhost/customer/account/updateSession/' + }; + + customerData['Magento_Customer/js/customer-data']($.extend({}, defaultConfig, config || {})); + } + + function setupLocalStorage(sections) { + var mageCacheStorage = {}, + sectionDataIds = {}; + + _.each(sections, function (sectionData, sectionName) { + sectionDataIds[sectionName] = sectionData['data_id']; + + if (typeof sectionData.content !== 'undefined') { + mageCacheStorage[sectionName] = sectionData; + } + }); + + $.localStorage.set( + 'mage-cache-storage', + mageCacheStorage + ); + $.cookieStorage.set( + 'section_data_ids', + sectionDataIds + ); + + $.localStorage.set( + 'mage-cache-timeout', + new Date(Date.now() + cookieLifeTime * 1000) + ); + $.cookieStorage.set( + 'mage-cache-sessid', + true + ); + } + + function clearLocalStorage() { + $.cookieStorage.set('section_data_ids', {}); + + if (window.localStorage) { + window.localStorage.clear(); + } + } + + describe('Magento_Customer/js/customer-data', function () { + beforeAll(function () { + clearLocalStorage(); + }); + + beforeEach(function () { + jQueryGetJSON = $.getJSON; + sectionConfig['Magento_Customer/js/section-config'](sectionConfigSettings); + }); + + afterEach(function () { + $.getJSON = jQueryGetJSON; + clearLocalStorage(); + }); + + describe('getExpiredSectionNames()', function () { + it('check that result contains expired section names', function () { + setupLocalStorage({ + 'cart': { + 'data_id': Math.floor(Date.now() / 1000) - 61 * 60, // 61 minutes ago + 'content': {} + } + }); + init(); + expect(customerData.getExpiredSectionNames()).toEqual(['cart']); + }); + + it('check that result doest not contain unexpired section names', function () { + setupLocalStorage({ + 'cart': { + 'data_id': Math.floor(Date.now() / 1000) + 60, // in 1 minute + 'content': {} + } + }); + init(); + expect(customerData.getExpiredSectionNames()).toEqual([]); + }); + + it('check that result contains invalidated section names', function () { + setupLocalStorage({ + 'cart': { // without storage content + 'data_id': Math.floor(Date.now() / 1000) + 60 // in 1 minute + } + }); + + init(); + expect(customerData.getExpiredSectionNames()).toEqual(['cart']); + }); + + it('check that result does not contain unsupported section names', function () { + setupLocalStorage({ + 'catalog': { // without storage content + 'data_id': Math.floor(Date.now() / 1000) + 60 // in 1 minute + } + }); + + init(); + expect(customerData.getExpiredSectionNames()).toEqual([]); + }); + }); + + describe('init()', function () { + it('check that sections are not requested from server, if there are no expired sections', function () { + setupLocalStorage({ + 'catalog': { // without storage content + 'data_id': Math.floor(Date.now() / 1000) + 60 // in 1 minute + } + }); + + $.getJSON = jasmine.createSpy('$.getJSON').and.callFake(function () { + var deferred = $.Deferred(); + + return deferred.promise(); + }); + + init(); + expect($.getJSON).not.toHaveBeenCalled(); + }); + it('check that sections are requested from server, if there are expired sections', function () { + setupLocalStorage({ + 'customer': { + 'data_id': Math.floor(Date.now() / 1000) + 60 // invalidated, + }, + 'cart': { + 'data_id': Math.floor(Date.now() / 1000) - 61 * 60, // 61 minutes ago + 'content': {} + }, + 'product_data_storage': { + 'data_id': Math.floor(Date.now() / 1000) + 60, // in 1 minute + 'content': {} + }, + 'catalog': { + 'data_id': Math.floor(Date.now() / 1000) + 60 // invalid section, + }, + 'checkout': { + 'data_id': Math.floor(Date.now() / 1000) - 61 * 60, // invalid section, + 'content': {} + } + }); + + $.getJSON = jasmine.createSpy('$.getJSON').and.callFake(function () { + var deferred = $.Deferred(); + + return deferred.promise(); + }); + + init(); + expect($.getJSON).toHaveBeenCalledWith( + 'http://localhost/customer/section/load/', + jasmine.objectContaining({ + sections: 'cart,customer' + }) + ); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js index f46ff6b30abbe..c0ecec40516fa 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/ui-select.test.js @@ -246,6 +246,12 @@ define([ expect(type).toEqual('object'); }); + it('Must be false if "disabled" is true', function () { + obj.listVisible(false); + obj.disabled(true); + obj.toggleListVisible(); + expect(obj.listVisible()).toEqual(false); + }); it('Must be false if "listVisible" is true', function () { obj.listVisible(true); obj.toggleListVisible(); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js index 6a466f0c37872..a5b434d956097 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js @@ -74,6 +74,7 @@ define([ originMock = $.fn.get; spyOn($.fn, 'get').and.returnValue(imageMock); + imagePreview.lastOpenedImage = jasmine.createSpy().and.returnValue(2); imagePreview.visibleRecord = jasmine.createSpy().and.returnValue(2); imagePreview.displayedRecord = ko.observable(); imagePreview.displayedRecord(recordMock); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js index 2c2cdab2d46da..7f7d0c5f9dd2a 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js @@ -6,79 +6,78 @@ /*eslint max-nested-callbacks: 0*/ define([ 'jquery', - 'ko', 'Magento_Ui/js/grid/masonry' -], function ($, ko, Masonry) { +], function ($, Masonry) { 'use strict'; - var Component, - rows, - container = '<div data-id="masonry_grid" id="masonry_grid"><div class="masonry-image-column"></div></div>'; + describe('Magento_Ui/js/grid/masonry', function () { + var Component, + rows, + container = '<div data-id="masonry_grid" id="masonry_grid"><div class="masonry-image-column"></div></div>'; - beforeEach(function () { - rows = [ - { - _rowIndex: 0, - category: {}, - 'category_id': 695, - 'category_name': 'People', - 'comp_url': 'https://stock.adobe.com/Rest/Libraries/Watermarked/Download/327515738/2', - 'content_type': 'image/jpeg', - 'country_name': 'Malaysia', - 'creation_date': '2020-03-02 10:41:51', - 'creator_id': 208217780, - 'creator_name': 'NajmiArif', - height: 3264, - id: 327515738, - 'id_field_name': 'id', - 'is_downloaded': 0, - 'is_licensed_locally': 0, - keywords: [], - 'media_type_id': 1, - overlay: '', - path: '', - 'premium_level_id': 0, - 'thumbnail_240_url': 'https://t4.ftcdn.net/jpg/03/27/51/57/240_F_327515738_n.jpg', - 'thumbnail_500_ur': 'https://as2.ftcdn.net/jpg/03/27/51/57/500_F_327515738_n.jpg', - title: 'Neon effect picture of man wearing medical mask for viral or pandemic disease', - width: 4896 - } + beforeEach(function () { + rows = [ + { + _rowIndex: 0, + category: {}, + 'category_id': 695, + 'category_name': 'People', + 'comp_url': 'url', + 'content_type': 'image/jpeg', + 'country_name': 'Malaysia', + 'creation_date': '2020-03-02 10:41:51', + 'creator_id': 208217780, + 'creator_name': 'NajmiArif', + height: 3264, + id: 327515738, + 'id_field_name': 'id', + 'is_downloaded': 0, + 'is_licensed_locally': 0, + keywords: [], + 'media_type_id': 1, + overlay: '', + path: '', + 'premium_level_id': 0, + 'thumbnail_240_url': 'url', + 'thumbnail_500_ur': 'url', + title: 'Neon effect picture of man wearing medical mask for viral or pandemic disease', + width: 4896 + } + ]; - ]; - - $(container).appendTo('body'); - - Component = new Masonry({ - defaults: { - rows: ko.observable() - } + $(document.body).append(container); + Component = new Masonry({ + defaults: { + containerId: '#masonry_grid' + } + }); }); - }); - - afterEach(function () { - $('#masonry_grid').remove(); - }); + afterEach(function () { + Component.clear(); + $('#masonry_grid').remove(); + }); - describe('check initComponent', function () { - it('verify setLayoutstyles called and grid iniztilized', function () { - var setlayoutStyles = spyOn(Component, 'setLayoutStyles'); + describe('check initComponent', function () { + it('verify setLayoutstyles called and grid iniztilized', function () { + var setlayoutStyles = spyOn(Component, 'setLayoutStyles'); - expect(Component).toBeDefined(); - Component.containerId = 'masonry_grid'; - Component.initComponent(rows); - Component.rows().forEach(function (image) { - expect(image.styles).toBeDefined(); - expect(image.css).toBeDefined(); + expect(Component).toBeDefined(); + Component.containerId = 'masonry_grid'; + Component.initComponent(rows); + Component.rows().forEach(function (image) { + expect(image.styles).toBeDefined(); + expect(image.css).toBeDefined(); + }); + expect(setlayoutStyles).toHaveBeenCalled(); }); - expect(setlayoutStyles).toHaveBeenCalled(); - }); - it('verify events triggered', function () { - var setLayoutStyles = spyOn(Component, 'setLayoutStyles'); + it('verify events triggered', function () { + var setLayoutStyles = spyOn(Component, 'setLayoutStyles'); - Component.initComponent(rows); - window.dispatchEvent(new Event('resize')); - expect(setLayoutStyles).toHaveBeenCalled(); + Component.initComponent(rows); + window.dispatchEvent(new Event('resize')); + expect(setLayoutStyles).toHaveBeenCalled(); + }); }); }); }); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/url-filter-applier.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/url-filter-applier.test.js index a3d49e382de51..1e63f9f61f6d1 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/url-filter-applier.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/url-filter-applier.test.js @@ -12,7 +12,8 @@ define([ describe('Magento_Ui/js/grid/url-filter-applier', function () { var urlFilterApplierObj, filterComponentMock = { - setData: jasmine.createSpy(), + set: jasmine.createSpy(), + get: jasmine.createSpy(), apply: jasmine.createSpy() }; @@ -64,11 +65,14 @@ define([ it('applies url filter on filter component', function () { urlFilterApplierObj.searchString = '?filters[name]=test&filters[qty]=1'; urlFilterApplierObj.apply(); - expect(urlFilterApplierObj.filterComponent().setData).toHaveBeenCalledWith({ - 'name': 'test', - 'qty': '1' - }, false); - expect(urlFilterApplierObj.filterComponent().apply).toHaveBeenCalled(); + expect(urlFilterApplierObj.filterComponent().get).toHaveBeenCalled(); + expect(urlFilterApplierObj.filterComponent().set).toHaveBeenCalledWith( + 'applied', + { + 'name': 'test', + 'qty': '1' + } + ); }); }); }); diff --git a/lib/internal/Magento/Framework/DB/Select.php b/lib/internal/Magento/Framework/DB/Select.php index 7d2799cf50679..0aaf29aeb332e 100644 --- a/lib/internal/Magento/Framework/DB/Select.php +++ b/lib/internal/Magento/Framework/DB/Select.php @@ -116,11 +116,11 @@ public function where($cond, $value = null, $type = null) { if ($value === null && $type === null) { $value = ''; - } elseif ($type == self::TYPE_CONDITION) { + } elseif ((string)$type === self::TYPE_CONDITION) { $type = null; } if (is_array($value)) { - $cond = $this->getConnection()->quoteInto($cond, $value); + $cond = $this->getConnection()->quoteInto($cond, $value, $type); $value = null; } return parent::where($cond, $value, $type); diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php index 20d017cc71062..073fb2e795e87 100644 --- a/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/Type.php @@ -73,7 +73,15 @@ public function getFields() : array /** * Get interfaces the type implements, if any. Return an empty array if none are configured. * - * @return string[] + * Example return array( + * array( + * 'interface' => 'SomeDefinedTypeInterface', + * 'copyFields' => true + * ), + * ... + * ), + * + * @return array */ public function getInterfaces() : array { diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionFactory.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionFactory.php new file mode 100644 index 0000000000000..02d0a60064db6 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionFactory.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\GraphQl\Config\Element; + +use Magento\Framework\GraphQl\Config\ConfigElementFactoryInterface; +use Magento\Framework\GraphQl\Config\ConfigElementInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Factory for config elements of 'union' type. + */ +class UnionFactory implements ConfigElementFactoryInterface +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct( + ObjectManagerInterface $objectManager + ) { + $this->objectManager = $objectManager; + } + + /** + * Instantiate an object representing 'union' GraphQL config element. + * + * @param array $data + * @return ConfigElementInterface + */ + public function createFromConfigData(array $data): ConfigElementInterface + { + return $this->create($data, $data['types'] ?? []); + } + + /** + * Create union object based off array of configured GraphQL. + * + * Union data must contain name, type resolver, and possible concrete types definitions + * The type resolver should point to an implementation of the TypeResolverInterface + * that decides what concrete GraphQL type to output. Description is the only optional field. + * + * @param array $unionData + * @param array $types + * @return UnionType + */ + public function create( + array $unionData, + array $types + ) : UnionType { + return $this->objectManager->create( + UnionType::class, + [ + 'name' => $unionData['name'], + 'typeResolver' => $unionData['typeResolver'], + 'types' => $types, + 'description' => isset($unionData['description']) ? $unionData['description'] : '' + ] + ); + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionInterface.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionInterface.php new file mode 100644 index 0000000000000..2d557e6dc5b84 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\GraphQl\Config\Element; + +use Magento\Framework\GraphQl\Config\ConfigElementInterface; + +/** + * Defines the contract for the union configuration data type. + */ +interface UnionInterface extends ConfigElementInterface +{ + /** + * Get a list of fields that make up the possible return or input values of a type. + * + * @return Type[] + */ + public function getTypes(): array; +} diff --git a/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionType.php b/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionType.php new file mode 100644 index 0000000000000..5b27d03360efc --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Config/Element/UnionType.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\GraphQl\Config\Element; + +/** + * Class representing 'union' GraphQL config element. + */ +class UnionType implements UnionInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var string[] + */ + private $types; + + /** + * @var string + */ + private $typeResolver; + + /** + * @var string + */ + private $description; + + /** + * @param string $name + * @param string $typeResolver + * @param string[] $types + * @param string $description + */ + public function __construct( + string $name, + string $typeResolver, + array $types, + string $description + ) { + $this->name = $name; + $this->types = $types; + $this->typeResolver = $typeResolver; + $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 string[] + */ + public function getTypes(): array + { + return $this->types; + } + + /** + * Return the name of the resolver class that determines the concrete type to display in the result. + * + * @return string + */ + public function getTypeResolver(): string + { + return $this->typeResolver; + } + + /** + * 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/Query/Resolver/Argument/SearchCriteria/ArgumentApplier/Filter.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/SearchCriteria/ArgumentApplier/Filter.php index 0e5feeba3bade..5ec80b1d4692c 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/SearchCriteria/ArgumentApplier/Filter.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/SearchCriteria/ArgumentApplier/Filter.php @@ -51,7 +51,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function applyArgument( SearchCriteriaInterface $searchCriteria, diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/SearchCriteria/ArgumentApplier/Sort.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/SearchCriteria/ArgumentApplier/Sort.php index 6ecb1896d685a..d312837e41686 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/SearchCriteria/ArgumentApplier/Sort.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/SearchCriteria/ArgumentApplier/Sort.php @@ -33,7 +33,7 @@ public function __construct(SortOrderBuilder $sortOrderBuilder = null) } /** - * {@inheritdoc} + * @inheritDoc */ public function applyArgument( SearchCriteriaInterface $searchCriteria, 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 ad9fb675a6d70..3456f3c039a64 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 @@ -10,6 +10,7 @@ use Magento\Framework\GraphQl\Config\Data\WrappedTypeProcessor; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Config\Element\TypeInterface; +use Magento\Framework\GraphQl\Config\ConfigElementInterface; use Magento\Framework\GraphQl\Schema\Type\Input\InputMapper; use Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\FormatterInterface; use Magento\Framework\GraphQl\Schema\Type\Output\OutputMapper; @@ -89,17 +90,20 @@ public function __construct( /** * @inheritdoc */ - public function format(TypeInterface $configElement, OutputTypeInterface $outputType): array + public function format(ConfigElementInterface $configElement, OutputTypeInterface $outputType): array { - $typeConfig = [ - 'fields' => function () use ($configElement, $outputType) { - $fieldsConfig = []; - foreach ($configElement->getFields() as $field) { - $fieldsConfig[$field->getName()] = $this->getFieldConfig($configElement, $outputType, $field); + $typeConfig = []; + if ($configElement instanceof TypeInterface) { + $typeConfig = [ + 'fields' => function () use ($configElement, $outputType) { + $fieldsConfig = []; + foreach ($configElement->getFields() as $field) { + $fieldsConfig[$field->getName()] = $this->getFieldConfig($configElement, $outputType, $field); + } + return $fieldsConfig; } - return $fieldsConfig; - } - ]; + ]; + } return $typeConfig; } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Interfaces.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Interfaces.php index 659c3f604508d..761ba68e44521 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Interfaces.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Interfaces.php @@ -8,7 +8,7 @@ namespace Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\Formatter; use Magento\Framework\GraphQl\Config\Element\Type; -use Magento\Framework\GraphQl\Config\Element\TypeInterface; +use Magento\Framework\GraphQl\Config\ConfigElementInterface; use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; use Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\FormatterInterface; use Magento\Framework\GraphQl\Schema\Type\Output\OutputMapper; @@ -32,9 +32,9 @@ public function __construct(OutputMapper $outputMapper) } /** - * {@inheritDoc} + * @inheritDoc */ - public function format(TypeInterface $configElement, OutputTypeInterface $outputType) : array + public function format(ConfigElementInterface $configElement, OutputTypeInterface $outputType) : array { $config = []; if ($configElement instanceof Type && !empty($configElement->getInterfaces())) { diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/ResolveType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/ResolveType.php index 3a40e609eb952..553e8fe40efc2 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/ResolveType.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/ResolveType.php @@ -8,7 +8,8 @@ namespace Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\Formatter; use Magento\Framework\GraphQl\Config\Element\InterfaceType; -use Magento\Framework\GraphQl\Config\Element\TypeInterface; +use Magento\Framework\GraphQl\Config\Element\UnionType; +use Magento\Framework\GraphQl\Config\ConfigElementInterface; use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; use Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\FormatterInterface; use Magento\Framework\ObjectManagerInterface; @@ -32,12 +33,12 @@ public function __construct(ObjectManagerInterface $objectManager) } /** - * {@inheritDoc} + * @inheritDoc */ - public function format(TypeInterface $configElement, OutputTypeInterface $outputType) : array + public function format(ConfigElementInterface $configElement, OutputTypeInterface $outputType) : array { $config = []; - if ($configElement instanceof InterfaceType) { + if ($configElement instanceof InterfaceType || $configElement instanceof UnionType) { $typeResolver = $this->objectManager->create($configElement->getTypeResolver()); $config['resolveType'] = function ($value) use ($typeResolver) { return $typeResolver->resolveType($value); diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Unions.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Unions.php new file mode 100644 index 0000000000000..75b6a58790a09 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/Formatter/Unions.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\Formatter; + +use Magento\Framework\GraphQl\Config\Element\UnionType; +use Magento\Framework\GraphQl\Config\ConfigElementInterface; +use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; +use Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper\FormatterInterface; +use Magento\Framework\GraphQl\Schema\Type\Output\OutputMapper; + +/** + * Add unions implemented by type if configured. + */ +class Unions implements FormatterInterface +{ + /** + * @var OutputMapper + */ + private $outputMapper; + + /** + * @param OutputMapper $outputMapper + */ + public function __construct(OutputMapper $outputMapper) + { + $this->outputMapper = $outputMapper; + } + + /** + * @inheritDoc + */ + public function format(ConfigElementInterface $configElement, OutputTypeInterface $outputType): array + { + $config = []; + if ($configElement instanceof UnionType && !empty($configElement->getTypes())) { + $unionTypes = []; + foreach ($configElement->getTypes() as $unionName) { + $unionTypes[$unionName] = $this->outputMapper->getOutputType($unionName); + } + $config['types'] = $unionTypes; + } + + return $config; + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/FormatterComposite.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/FormatterComposite.php index 416b4122b7097..c36df11012017 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/FormatterComposite.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/FormatterComposite.php @@ -7,11 +7,11 @@ namespace Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper; -use Magento\Framework\GraphQl\Config\Element\TypeInterface; +use Magento\Framework\GraphQl\Config\ConfigElementInterface; use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; /** - * {@inheritdoc} + * @inheritDoc */ class FormatterComposite implements FormatterInterface { @@ -29,18 +29,19 @@ public function __construct(array $formatters) } /** - * {@inheritDoc} + * @inheritDoc */ - public function format(TypeInterface $configElement, OutputTypeInterface $outputType) : array + public function format(ConfigElementInterface $configElement, OutputTypeInterface $outputType): array { - $config = [ + $defaultConfig = [ 'name' => $configElement->getName(), 'description' => $configElement->getDescription() ]; + $formattedConfig = []; foreach ($this->formatters as $formatter) { - $config = array_merge($config, $formatter->format($configElement, $outputType)); + $formattedConfig[] = $formatter->format($configElement, $outputType); } - return $config; + return array_merge($defaultConfig, ...$formattedConfig); } } diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/FormatterInterface.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/FormatterInterface.php index 7d40b743a6a06..a99237aa8843a 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/FormatterInterface.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/ElementMapper/FormatterInterface.php @@ -7,7 +7,7 @@ namespace Magento\Framework\GraphQl\Schema\Type\Output\ElementMapper; -use Magento\Framework\GraphQl\Config\Element\TypeInterface as TypeElementInterface; +use Magento\Framework\GraphQl\Config\ConfigElementInterface; use Magento\Framework\GraphQl\Schema\Type\OutputTypeInterface; /** @@ -18,9 +18,9 @@ interface FormatterInterface /** * Convert GraphQL config element to the object compatible with GraphQL schema generator. * - * @param TypeElementInterface $configElement + * @param ConfigElementInterface $configElement * @param OutputTypeInterface $outputType * @return array */ - public function format(TypeElementInterface $configElement, OutputTypeInterface $outputType) : array; + public function format(ConfigElementInterface $configElement, OutputTypeInterface $outputType): array; } 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 046eeb5b1f93d..afe0d84de26f8 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputMapper.php @@ -13,7 +13,7 @@ use Magento\Framework\Phrase; /** - * Map type names to their output type/interface/enum classes. + * Map type names to their output type/interface/union/enum classes. */ class OutputMapper { @@ -38,7 +38,7 @@ public function __construct( * @return OutputTypeInterface * @throws GraphQlInputException */ - public function getOutputType($typeName) + public function getOutputType(string $typeName) { $outputType = $this->typeRegistry->get($typeName); diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputUnionObject.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputUnionObject.php new file mode 100644 index 0000000000000..a63b7b7417527 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/Output/OutputUnionObject.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\GraphQl\Schema\Type\Output; + +use Magento\Framework\GraphQl\Config\Element\UnionType as UnionElement; +use Magento\Framework\GraphQl\Schema\Type\UnionType; + +/** + * The 'union' type compatible with GraphQL schema generator. + */ +class OutputUnionObject extends UnionType +{ + /** + * @param ElementMapper $elementMapper + * @param UnionElement $configElement + */ + public function __construct( + ElementMapper $elementMapper, + UnionElement $configElement + ) { + parent::__construct($elementMapper->buildSchemaArray($configElement, $this)); + } +} diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/Type/UnionType.php b/lib/internal/Magento/Framework/GraphQl/Schema/Type/UnionType.php new file mode 100644 index 0000000000000..a843c6c669acf --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Schema/Type/UnionType.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\GraphQl\Schema\Type; + +/** + * Wrapper for GraphQl UnionType + */ +class UnionType extends \GraphQL\Type\Definition\UnionType implements OutputTypeInterface +{ + +} diff --git a/lib/internal/Magento/Framework/GraphQl/Schema/TypeFactory.php b/lib/internal/Magento/Framework/GraphQl/Schema/TypeFactory.php index 3b31653382e3a..84f8d809c958e 100644 --- a/lib/internal/Magento/Framework/GraphQl/Schema/TypeFactory.php +++ b/lib/internal/Magento/Framework/GraphQl/Schema/TypeFactory.php @@ -13,7 +13,7 @@ use Magento\Framework\GraphQl\Schema\Type\EnumType; use Magento\Framework\GraphQl\Schema\Type\ListOfType; use Magento\Framework\GraphQl\Schema\Type\NonNull; -use Magento\Framework\GraphQl\Schema\TypeInterface; +use Magento\Framework\GraphQl\Schema\Type\UnionType; /** * Factory for @see TypeInterface implementations @@ -42,6 +42,17 @@ public function createInterface(array $config) : InterfaceType return new InterfaceType($config); } + /** + * Create an union type + * + * @param array $config + * @return UnionType + */ + public function createUnion(array $config) : UnionType + { + return new UnionType($config); + } + /** * Create an input object type * diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php index 1e8b33f79854b..9845646e09fd5 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php @@ -11,6 +11,7 @@ use Magento\Framework\Config\FileResolverInterface; use Magento\Framework\Config\ReaderInterface; use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface as TypeReaderComposite; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\Reader\InterfaceType; /** * Reads *.graphqls files from modules and combines the results as array to be used with a library to configure objects @@ -21,6 +22,7 @@ class GraphQlReader implements ReaderInterface public const GRAPHQL_SCHEMA_FILE = 'schema.graphqls'; + /** @deprecated */ public const GRAPHQL_INTERFACE = 'graphql_interface'; /** @@ -69,7 +71,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc * * @param string|null $scope * @return array @@ -178,7 +180,7 @@ private function parseTypes(string $graphQlSchemaContent) : array private function copyInterfaceFieldsToConcreteTypes(array $source): array { foreach ($source as $interface) { - if ($interface['type'] == 'graphql_interface') { + if ($interface['type'] ?? '' == InterfaceType::GRAPHQL_INTERFACE) { foreach ($source as $typeName => $type) { if (isset($type['implements']) && isset($type['implements'][$interface['name']]) @@ -253,7 +255,7 @@ private function convertInterfacesToAnnotations(string $graphQlSchemaContent): s private function addPlaceHolderInSchema(string $graphQlSchemaContent) :string { $placeholderField = self::GRAPHQL_PLACEHOLDER_FIELD_NAME; - $typesKindsPattern = '(type|interface|input)'; + $typesKindsPattern = '(type|interface|input|union)'; $enumKindsPattern = '(enum)'; $typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+)'; $typeDefinitionPattern = '([^\{]*)(\{[\s\t\n\r^\}]*\})'; @@ -328,13 +330,14 @@ private static function getModuleNameForRelevantFile(string $file): string */ private function addModuleNameToTypes(array $source, string $filePath): array { - foreach ($source as $typeName => $type) { - if (!isset($type['module']) && ( - ($type['type'] === self::GRAPHQL_INTERFACE && isset($type['typeResolver'])) - || isset($type['implements']) - ) - ) { - $source[$typeName]['module'] = self::getModuleNameForRelevantFile($filePath); + foreach ($source as $typeName => $typeDefinition) { + if (!isset($typeDefinition['module'])) { + $hasTypeResolver = (bool)($typeDefinition['typeResolver'] ?? false); + $hasImplements = (bool)($typeDefinition['implements'] ?? false); + $typeDefinition = (bool)($typeDefinition['type'] ?? false); + if ((($typeDefinition === InterfaceType::GRAPHQL_INTERFACE && $hasTypeResolver) || $hasImplements)) { + $source[$typeName]['module'] = self::getModuleNameForRelevantFile($filePath); + } } } diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/EnumType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/EnumType.php index e4dec7afdab0a..c6e0481c7490e 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/EnumType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/EnumType.php @@ -15,6 +15,8 @@ */ class EnumType implements TypeMetaReaderInterface { + public const GRAPHQL_ENUM = 'graphql_enum'; + /** * @var DocReader */ @@ -30,14 +32,14 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { if ($typeMeta instanceof \GraphQL\Type\Definition\EnumType) { $result = [ 'name' => $typeMeta->name, - 'type' => 'graphql_enum', + 'type' => self::GRAPHQL_ENUM, 'items' => [] // Populated later ]; foreach ($typeMeta->getValues() as $enumValueMeta) { diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php index 38159fac03b3b..2108a04d8a9ed 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InputObjectType.php @@ -17,6 +17,8 @@ */ class InputObjectType implements TypeMetaReaderInterface { + public const GRAPHQL_INPUT = 'graphql_input'; + /** * @var TypeMetaWrapperReader */ @@ -49,7 +51,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { @@ -57,7 +59,7 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array $typeName = $typeMeta->name; $result = [ 'name' => $typeName, - 'type' => 'graphql_input', + 'type' => self::GRAPHQL_INPUT, 'fields' => [] // Populated later ]; $fields = $typeMeta->getFields(); diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php index baadb4be61cf2..76550469d409e 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/InterfaceType.php @@ -17,6 +17,8 @@ */ class InterfaceType implements TypeMetaReaderInterface { + public const GRAPHQL_INTERFACE = 'graphql_interface'; + /** * @var FieldMetaReader */ @@ -49,7 +51,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { @@ -57,7 +59,7 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array $typeName = $typeMeta->name; $result = [ 'name' => $typeName, - 'type' => 'graphql_interface', + 'type' => self::GRAPHQL_INTERFACE, 'fields' => [] ]; diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php index ba8e46dd60557..3ad6d69eb5c9c 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/ObjectType.php @@ -19,6 +19,8 @@ */ class ObjectType implements TypeMetaReaderInterface { + public const GRAPHQL_TYPE = 'graphql_type'; + /** * @var FieldMetaReader */ @@ -69,7 +71,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { @@ -77,7 +79,7 @@ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array $typeName = $typeMeta->name; $result = [ 'name' => $typeName, - 'type' => 'graphql_type', + 'type' => self::GRAPHQL_TYPE, 'fields' => [], // Populated later ]; diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/UnionType.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/UnionType.php new file mode 100644 index 0000000000000..5776901765b5a --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/Reader/UnionType.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\GraphQlSchemaStitching\GraphQlReader\Reader; + +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\CacheAnnotationReader; + +/** + * Composite configuration reader to handle the union object type meta + */ +class UnionType implements TypeMetaReaderInterface +{ + public const GRAPHQL_UNION = 'graphql_union'; + + /** + * @var FieldMetaReader + */ + private $fieldMetaReader; + + /** + * @var DocReader + */ + private $docReader; + + /** + * @var CacheAnnotationReader + */ + private $cacheAnnotationReader; + + /** + * @param FieldMetaReader $fieldMetaReader + * @param DocReader $docReader + * @param CacheAnnotationReader|null $cacheAnnotationReader + */ + public function __construct( + FieldMetaReader $fieldMetaReader, + DocReader $docReader, + CacheAnnotationReader $cacheAnnotationReader = null + ) { + $this->fieldMetaReader = $fieldMetaReader; + $this->docReader = $docReader; + $this->cacheAnnotationReader = $cacheAnnotationReader ?? \Magento\Framework\App\ObjectManager::getInstance() + ->get(CacheAnnotationReader::class); + } + + /** + * @inheritDoc + */ + public function read(\GraphQL\Type\Definition\Type $typeMeta): array + { + if ($typeMeta instanceof \GraphQL\Type\Definition\UnionType) { + $typeName = $typeMeta->name; + $result = [ + 'name' => $typeName, + 'type' => self::GRAPHQL_UNION, + 'types' => [], + ]; + + $unionTypeResolver = $this->getUnionTypeResolver($typeMeta); + if (!empty($unionTypeResolver)) { + $result['typeResolver'] = $unionTypeResolver; + } + + foreach ($typeMeta->getTypes() as $type) { + $result['types'][] = $type->name; + } + + if ($this->docReader->read($typeMeta->astNode->directives)) { + $result['description'] = $this->docReader->read($typeMeta->astNode->directives); + } + + return $result; + } else { + return []; + } + } + + /** + * Retrieve the union type resolver if it exists from the meta data + * + * @param \GraphQL\Type\Definition\UnionType $unionTypeMeta + * @return string + */ + private function getUnionTypeResolver(\GraphQL\Type\Definition\UnionType $unionTypeMeta): string + { + /** @var \GraphQL\Language\AST\NodeList $directives */ + $directives = $unionTypeMeta->astNode->directives; + foreach ($directives as $directive) { + if ($directive->name->value == 'typeResolver') { + foreach ($directive->arguments as $directiveArgument) { + if ($directiveArgument->name->value == 'class') { + return $directiveArgument->value->value; + } + } + } + } + return ''; + } +} diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/TypeReaderComposite.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/TypeReaderComposite.php index e9b899bb2bb5b..614c1e3a743a0 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/TypeReaderComposite.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader/TypeReaderComposite.php @@ -25,7 +25,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function read(\GraphQL\Type\Definition\Type $typeMeta) : array { diff --git a/lib/internal/Magento/Framework/Registry.php b/lib/internal/Magento/Framework/Registry.php index e798b28e1e1b2..b5944729fd1a1 100644 --- a/lib/internal/Magento/Framework/Registry.php +++ b/lib/internal/Magento/Framework/Registry.php @@ -9,7 +9,7 @@ * Registry model. Used to manage values in registry * * Registry usage as a shared service introduces temporal, hard to detect coupling into system. - * It's usage should be avoid. Use service classes or data providers instead. + * Its usage should be avoided. Use service classes or data providers instead. * * @api * @deprecated 102.0.0 diff --git a/lib/web/mage/translate-inline.js b/lib/web/mage/translate-inline.js index 56acef5a49a42..2407a64e5e0d1 100644 --- a/lib/web/mage/translate-inline.js +++ b/lib/web/mage/translate-inline.js @@ -6,9 +6,10 @@ define([ 'jquery', 'mage/template', - 'jquery-ui-modules/dialog', - 'mage/translate' -], function ($, mageTemplate) { + 'mage/utils/misc', + 'mage/translate', + 'jquery-ui-modules/dialog' +], function ($, mageTemplate, miscUtils) { 'use strict'; $.widget('mage.translateInline', $.ui.dialog, { @@ -59,11 +60,12 @@ define([ * Open. */ open: function () { - var topMargin; + var $uiDialog = $(this).closest('.ui-dialog'), + topMargin = $uiDialog.children('.ui-dialog-titlebar').outerHeight() + 45; - $(this).closest('.ui-dialog').addClass('ui-dialog-active'); - topMargin = jQuery(this).closest('.ui-dialog').children('.ui-dialog-titlebar').outerHeight() + 45; - jQuery(this).closest('.ui-dialog').css('margin-top', topMargin); + $uiDialog + .addClass('ui-dialog-active') + .css('margin-top', topMargin); }, /** @@ -79,11 +81,15 @@ define([ * @protected */ _create: function () { + var $translateArea = $(this.options.translateArea); + + if (!$translateArea.length) { + $translateArea = $('body'); + } + $translateArea.on('edit.editTrigger', $.proxy(this._onEdit, this)); + this.tmpl = mageTemplate(this.options.translateForm.template); - (this.options.translateArea && $(this.options.translateArea).length ? - $(this.options.translateArea) : - this.element.closest('body')) - .on('edit.editTrigger', $.proxy(this._onEdit, this)); + this._super(); }, @@ -95,7 +101,7 @@ define([ _prepareContent: function (templateData) { var data = $.extend({ items: templateData, - escape: $.mage.escapeHTML + escape: miscUtils.escape }, this.options.translateForm.data); this.data = data; @@ -131,12 +137,11 @@ define([ * @protected */ _formSubmit: function () { - var parameters; + var parameters = $.param({ + area: this.options.area + }) + '&' + $('#' + this.options.translateForm.data.id).serialize(); this.formIsSubmitted = true; - parameters = $.param({ - area: this.options.area - }) + '&' + $('#' + this.options.translateForm.data.id).serialize(); $.ajax({ url: this.options.ajaxUrl, @@ -162,11 +167,13 @@ define([ * @private */ _updatePlaceholder: function (newValue) { - var target = jQuery(this.target); + var $target = $(this.target), + translateObject = $target.data('translate')[0]; + + translateObject.shown = newValue; + translateObject.translated = newValue; - target.data('translate')[0].shown = newValue; - target.data('translate')[0].translated = newValue; - target.html(newValue); + $target.html(newValue); }, /** @@ -177,20 +184,6 @@ define([ this._super(); } }); - // @TODO move the "escapeHTML" method into the file with global utility functions - $.extend(true, $, { - mage: { - /** - * @param {String} str - * @return {Boolean} - */ - escapeHTML: function (str) { - return str ? - jQuery('<div/>').text(str).html().replace(/"/g, '"') : - false; - } - } - }); return $.mage.translateInline; }); diff --git a/lib/web/mage/utils/misc.js b/lib/web/mage/utils/misc.js index 3829f5ed467e2..b1c0c33324c28 100644 --- a/lib/web/mage/utils/misc.js +++ b/lib/web/mage/utils/misc.js @@ -6,8 +6,8 @@ define([ 'underscore', 'jquery', - 'FormData' -], function (_, $) { + 'mage/utils/objects' +], function (_, $, utils) { 'use strict'; var defaultAttributes, @@ -120,7 +120,7 @@ define([ */ submit: function (options, attrs) { var form = document.createElement('form'), - data = this.serialize(options.data), + data = utils.serialize(options.data), attributes = _.extend({}, defaultAttributes, attrs || {}); if (!attributes.action) { @@ -205,11 +205,11 @@ define([ if (type === 'default') { formData = new FormData(); - _.each(this.serialize(data), function (val, name) { + _.each(utils.serialize(data), function (val, name) { formData.append(name, val); }); } else if (type === 'simple') { - formData = this.serialize(data); + formData = utils.serialize(data); } return formData; @@ -242,6 +242,16 @@ define([ return data; }, + /** + * Replaces special characters with their corresponding HTML entities. + * + * @param {String} string - Text to escape. + * @returns {String} Escaped text. + */ + escape: function (string) { + return string ? $('<p/>').text(string).html().replace(/"/g, '"') : string; + }, + /** * Replaces symbol codes with their unescaped counterparts. * diff --git a/lib/web/mage/utils/objects.js b/lib/web/mage/utils/objects.js index 83711c43d2d1b..82866c0f2a6ad 100644 --- a/lib/web/mage/utils/objects.js +++ b/lib/web/mage/utils/objects.js @@ -5,8 +5,9 @@ define([ 'ko', 'jquery', - 'underscore' -], function (ko, $, _) { + 'underscore', + 'mage/utils/strings' +], function (ko, $, _, stringUtils) { 'use strict'; var primitives = [ @@ -217,7 +218,7 @@ define([ data = this.flatten(data); _.each(data, function (value, keys) { - keys = this.serializeName(keys); + keys = stringUtils.serializeName(keys); value = _.isUndefined(value) ? '' : value; result[keys] = value; diff --git a/package.json.sample b/package.json.sample index 4dea6d7b945f5..93fe72afbd24a 100644 --- a/package.json.sample +++ b/package.json.sample @@ -18,7 +18,7 @@ "grunt-contrib-connect": "~1.0.2", "grunt-contrib-cssmin": "~2.2.1", "grunt-contrib-imagemin": "~2.0.1", - "grunt-contrib-jasmine": "~1.1.0", + "grunt-contrib-jasmine": "~1.2.0", "grunt-contrib-less": "~1.4.1", "grunt-contrib-watch": "~1.0.0", "grunt-eslint": "~20.1.0", @@ -39,4 +39,4 @@ "time-grunt": "~1.4.0", "underscore": "~1.8.0" } -} \ No newline at end of file +} diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php index e864a81ffcc0e..4a3a02b37a6ab 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Session.php @@ -40,7 +40,7 @@ class Session implements ConfigOptionsListInterface const INPUT_KEY_SESSION_REDIS_SENTINEL_SERVERS = 'session-save-redis-sentinel-servers'; const INPUT_KEY_SESSION_REDIS_SENTINEL_MASTER = 'session-save-redis-sentinel-master'; const INPUT_KEY_SESSION_REDIS_SENTINEL_VERIFY_MASTER = 'session-save-redis-sentinel-verify-master'; - const INPUT_KEY_SESSION_REDIS_SENTINEL_CONNECT_RETRIES = 'session-save-redis-sentinel-connect-retires'; + const INPUT_KEY_SESSION_REDIS_SENTINEL_CONNECT_RETRIES = 'session-save-redis-sentinel-connect-retries'; const CONFIG_PATH_SESSION_REDIS = 'session/redis'; const CONFIG_PATH_SESSION_REDIS_HOST = 'session/redis/host'; diff --git a/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php b/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php index ba57c95999284..4ecbfd3deebf8 100644 --- a/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php +++ b/setup/src/Magento/Setup/Model/FixtureGenerator/CustomerTemplateGenerator.php @@ -10,6 +10,8 @@ use Magento\Customer\Model\AddressFactory; use Magento\Customer\Model\Customer; use Magento\Customer\Model\CustomerFactory; +use Magento\Directory\Model\ResourceModel\Region\CollectionFactory as RegionCollectionFactory; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; /** @@ -32,19 +34,29 @@ class CustomerTemplateGenerator implements TemplateEntityGeneratorInterface */ private $storeManager; + /** + * @var RegionCollectionFactory + */ + private $regionsCollectionFactory; + /** * @param CustomerFactory $customerFactory * @param AddressFactory $addressFactory * @param StoreManagerInterface $storeManager + * @param RegionCollectionFactory|null $regionsCollectionFactory */ public function __construct( CustomerFactory $customerFactory, AddressFactory $addressFactory, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + RegionCollectionFactory $regionsCollectionFactory = null ) { $this->customerFactory = $customerFactory; $this->addressFactory = $addressFactory; $this->storeManager = $storeManager; + $this->regionsCollectionFactory = $regionsCollectionFactory ?: ObjectManager::getInstance()->get( + RegionCollectionFactory::class + ); } /** @@ -119,7 +131,7 @@ private function getAddressTemplate($customerId) 'street' => 'Green str, 67', 'lastname' => 'Smith', 'firstname' => 'John', - 'region_id' => 1, + 'region_id' => $this->getFirstRegionId(), 'fax' => '04040404', 'middlename' => '', 'prefix' => '', @@ -131,4 +143,18 @@ private function getAddressTemplate($customerId) ] ]); } + + /** + * Get first region id. + * + * @return mixed + */ + private function getFirstRegionId() + { + $regionsCollection = $this->regionsCollectionFactory->create(); + $regionsCollection->unshiftOrder('region_id', 'ASC'); + $region = $regionsCollection->getFirstItem(); + + return $region->getRegionId(); + } }